Skip to main content

Advanced: Multi-Agent Custom Proposals

Organize a specialist sub-agent team with the multiagent coordinator pattern to produce a tailored sales proposal, observing every sub-agent precisely through the Session Threads API and thread-level events.

Overview

This page shows how to use the Managed Agents multiagent coordinator pattern to organize a team of specialist sub-agents that collaborate on a sales proposal, and how to observe and render each sub-agent precisely with the Session Threads API and the session-level event stream. Examples use curl (REST). Scenario: the fictional company Northstar (selling a workflow automation platform to mid-market ops teams) wants automated sales proposals. Today, reps hand-craft each prospect's proposal — research what companies in that segment care about, pick two relevant case studies, price by the internal rules sheet, assemble a two-page document. Every step uses different data and judgment. One coordinator agent runs three specialists: a researcher uses web search for segment priorities; a case-study picker reads the library for the two best matches; a pricing modeler reads only the rules file and seat count. Add a checker that sanity-checks alignment before the coordinator writes. The coordinator sequences them and writes the proposal.

Environment variables and models

export AGENTSTUDIO_URL="https://{workspace_id}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio"
export DASHSCOPE_API_KEY="<your DashScope API Key>"
All requests carry Authorization: Bearer $DASHSCOPE_API_KEY. The multiagent configuration and session event streams are Managed Agents capabilities, currently cn-beijing only. Use model IDs qwen3.8-max / qwen3.7-max / qwen3.7-plus. Every roster member is a full agent, and you can mix models per role — flagship models for high-value writing/coordination, cheaper tiers for mechanical file-reading and rule application, cost-split by model ID. This example uses qwen3.7-plus throughout for simplicity. Two frequent issues in multi-agent scenarios, stated up front:
Choosing a web search path. This example's researcher uses the marketplace search service (WebSearch, tool bailian_web_search); the built-in toolkit's web_search / web_fetch also covers web retrieval. Both paths work — but the MCP tool names differ from the built-ins, so the tool names referenced in the system prompt must match what is actually enabled.
default_config.enabled: true does not deliver tools to the model. Both builtin_toolkit and mcp_toolkit require every tool explicitly enabled: true in configs[ ], otherwise the agent has only the auto-injected mark_artifacts (the getting-started page covers this; it applies to team members too).

Define the three specialist sub-agents

Each sub-agent has its own system prompt, output shape, and only the tools it needs. The researcher gets only search; the picker only reads the local library; the pricing modeler sees only pricing_rules.md. Scoping tools by role prevents the modeler from pulling competitor prices off the web and keeps the whole case library out of the coordinator's context. The orchestration tools are auto-injected by the platform in multiagent mode — no need to declare them in tools (the coordinator receives create_agent / list_agents / wait_for_agents; workers receive submit_result).
Record the id returned by each POST /agents below (shaped like agent_<ULID>) — they go into the coordinator's roster.

The researcher prospect_researcher (search MCP only)

curl -X POST "$AGENTSTUDIO_URL/agents" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prospect_researcher",
    "description": "Researches what companies in a given industry segment and size tier typically prioritize.",
    "model": { "id": "qwen3.7-plus" },
    "system": "你是行业研究员。收到潜客的行业细分与规模档后,用 bailian_web_search 工具搜索:该细分的战略优先级、近期动作/趋势、常见运营痛点。完成后返回严格 JSON:{\"priorities\":[...],\"recent_moves\":[...],\"pain_points\":[...],\"sources\":[...]}。只做调研,不写提案。",
    "mcp_servers": [ { "type": "official", "name": "WebSearch" } ],
    "tools": [
      { "type": "builtin_toolkit", "default_config": { "enabled": false },
        "configs": [ { "name": "read", "enabled": true } ] },
      { "type": "mcp_toolkit", "mcp_server_name": "WebSearch",
        "default_config": { "enabled": true },
        "configs": [ { "name": "bailian_web_search", "enabled": true } ] }
    ]
  }'
mcp_servers.name is the marketplace service code (not existence-checked at declaration; a typo fails silently — troubleshooting in the production page). Marketplace services are called through the platform proxy; no credentials needed. To see a service's tool names: bl mcp tools --server WebSearch.

The case-study picker case_study_picker (local library only)

curl -X POST "$AGENTSTUDIO_URL/agents" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "case_study_picker",
    "description": "Selects the two most relevant case studies for a given prospect.",
    "model": { "id": "qwen3.7-plus" },
    "system": "案例库在 /mnt/session/uploads/case_studies/,每个文件是一个客户故事。收到潜客的行业/规模/优先级后,读遍案例库,按与潜客优先级的契合度打分,挑出最匹配的两个。返回严格 JSON:{\"picks\":[{\"file\":...,\"customer\":...,\"why_relevant\":...},...]}。你只能读本地文件,不联网。",
    "tools": [
      { "type": "builtin_toolkit", "default_config": { "enabled": false },
        "configs": [
          { "name": "read", "enabled": true },
          { "name": "glob", "enabled": true },
          { "name": "grep", "enabled": true }
        ] }
    ]
  }'

The pricing modeler pricing_modeler (rules file only)

curl -X POST "$AGENTSTUDIO_URL/agents" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "pricing_modeler",
    "description": "Builds two or three pricing options from the rules file and a seat count.",
    "model": { "id": "qwen3.7-plus" },
    "system": "定价规则在 /mnt/session/uploads/pricing_rules.md。收到席位数与用量档后,只依据规则文件建三档:保守(年付、低单价)、灵活(月付、高单价)、若席位>500 追加企业选项(含平台费)。每档给出首年总价。返回严格 JSON:{\"options\":[{\"name\":...,\"structure\":...,\"year_one_total\":...},...]}。禁止联网,禁止引用外部竞品价格。",
    "tools": [
      { "type": "builtin_toolkit", "default_config": { "enabled": false },
        "configs": [ { "name": "read", "enabled": true } ] }
    ]
  }'

Add a checker: pre-writing self-review

The three specialists each deliver a local conclusion; nobody is responsible for "do the three add up". So the roster gains a proposal_checker sub-agent: before the coordinator writes, it checks whether the picked cases and pricing framework align with the prospect's priorities. Delegated once before writing, it reads the research, picks, and pricing framework, judges self-consistency and priority fit, and returns a verdict plus suggestions. It is a plain worker like the others: delegable, message-passing, running in its own thread and context.
curl -X POST "$AGENTSTUDIO_URL/agents" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "proposal_checker",
    "description": "Sanity-checks whether the picked case studies and pricing framework match the prospect priorities before writing.",
    "model": { "id": "qwen3.7-plus" },
    "system": "你是提案校验员,在协调者动笔前做一次对齐检查。收到:潜客画像与优先级、已选的两个案例、定价三档框架。判断:所选案例是否命中潜客最关心的痛点?定价档位与席位/用量档是否自洽?有无与优先级脱节之处?返回严格 JSON:{\"aligned\":true|false,\"issues\":[...],\"suggestions\":[...]}。你只做判断,不改稿、不写提案。",
    "tools": [ ]
  }'
The checker is pure judgment with no tools (an empty tools array is fine — Advanced: Prompt Versioning and Rollback verified that a tool-less pure-LLM agent runs normally). For judgment-heavy work, set its model.id to a stronger model than the other workers — capability and cost tiering is done by choosing different model IDs; the model field has no effort-style dial.
multiagent.agents[ ] member types: {"type":"agent","id":...} references a created sub-agent, optionally with version (see below); {"type":"self"} makes the coordinator itself delegable — at most one; a second raises AGENT_010 multiagent.agents 中 self 类型最多只能有一个.

Give the team its materials and wire the coordinator

Materials

7 short case studies (healthcare/manufacturing/logistics/retail/finance/public sector) — enough to watch the picker choose the two that fit:
  • St. Clair Health: regional hospital network, 6,200 staff, credentialing and prior-auth scattered across 11 systems → merged into 3 automated flows, prior-auth turnaround down 58%, $1.9M saved annually.
  • BlueRidge Health Plan: regional payer, 2,800 staff, 19% of claims-exception emails needed rework → exception routing end-to-end automated, rework down to 6%, claims 11 days faster.
  • Calder Manufacturing: industrial, 3,100 staff, PO approvals averaged 9 days → PO cycle down to 2.1 days, maverick spend down 14%.
  • Northwind Logistics: 3PL, 4,400 staff, carrier onboarding 3 weeks each → down to 4 days, 22% more carriers activated in Q1.
  • Harborview Retail Group: specialty retail, 5,600 staff, store inventory anomalies over Slack+spreadsheets → triage automated across 140 stores, stockouts down 31%.
  • Aperture Payments: fintech, 1,900 staff, KYC and merchant onboarding averaged 6 business days → SLA down to 36 hours, 2.4× throughput with the same staff.
  • Summit County Government: public sector, 3,700 staff, paper building permits across five departments → single digital intake with parallel review, median permit time 41 → 17 days.
Product and pricing materials:
  • PRODUCT one-pager: Northstar is a workflow automation platform for mid-market ops teams — visual flow building, 200+ SaaS connectors, role-based approvals, SOC 2 Type II; typical results 40–60% fewer manual tickets, first workflow live in 3 weeks.
  • PRICING rules: $65/seat/month, or $52 annual; usage tiers light 1.0x / standard 1.15x / heavy 1.30x multiplied onto the unit price; enterprise tier (>500 seats) adds $48,000/yr platform fee and drops the annual rate to $44/seat/month; every option includes onboarding, enterprise adds a dedicated CSM.

Upload the 9 files

Upload the 7 case-study markdown files, product_one_pager.md, and pricing_rules.md one by one. Upload via multipart, then poll GET /files/{id} to status=available before mounting.
# Upload one by one; example for one case file
curl -X POST "$AGENTSTUDIO_URL/files" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -F "file=@st_clair_health.md"
# Take the file_id from the response, then poll:
# curl "$AGENTSTUDIO_URL/files/<file_id>" -H "Authorization: Bearer $DASHSCOPE_API_KEY"  → status=available
Repeat for all 9 files, recording each file_id.

Create the Environment (sandbox)

curl -X POST "$AGENTSTUDIO_URL/environments" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "northstar-proposal-env",
    "config": {
      "type": "cloud",
      "networking": { "type": "unrestricted" }
    }
  }'
The researcher's MCP search is proxied by the platform and does not traverse the sandbox network, but the picker and modeler read mounted files (sandbox tools), so the environment is still needed. networking.type currently takes unrestricted (all egress allowed). The response id looks like env_<...>.

Create the coordinator (with roster)

The coordinator references the four sub-agents (three specialists + checker):
curl -X POST "$AGENTSTUDIO_URL/agents" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "proposal_coordinator",
    "description": "Assembles a tailored sales proposal by orchestrating specialists.",
    "model": { "id": "qwen3.7-plus" },
    "system": "你是提案协调者,负责组装一份定制销售提案。给你潜客名和画像后,按顺序编排团队:\n1) 把行业和规模发给 prospect_researcher。\n2) 研究员回来后,把行业/规模/优先级发给 case_study_picker。\n3) 把席位数和用量档发给 pricing_modeler。\n4) 在动笔前,把研究结论、案例挑选、定价框架一起发给 proposal_checker 做对齐校验。若校验发现不对齐,据其 suggestions 调整案例或定价档再继续。\n5) 读 /mnt/session/uploads/product_one_pager.md,然后写 /mnt/session/outputs/proposal.md,包含:Executive summary(贴合潜客优先级)/ How we help(来自 one-pager)/ Proof(两个案例)/ Investment(定价选项)/ Next steps。控制在两页以内。\n你自己不做专家工作,只负责决定顺序、交接和最终写作。",
    "tools": [
      { "type": "builtin_toolkit", "default_config": { "enabled": false },
        "configs": [
          { "name": "read",  "enabled": true },
          { "name": "write", "enabled": true }
        ] }
    ],
    "multiagent": {
      "type": "coordinator",
      "agents": [
        { "type": "agent", "id": "<agent_id_prospect_researcher>" },
        { "type": "agent", "id": "<agent_id_case_study_picker>" },
        { "type": "agent", "id": "<agent_id_pricing_modeler>" },
        { "type": "agent", "id": "<agent_id_proposal_checker>" }
      ]
    }
  }'
Record the coordinator's id (agent_<ULID>) as $AGENT_ID.
Roster member version semantics: omitting version neither snapshots at coordinator creation nor at session creation — leave the coordinator untouched, upgrade pricing_modeler from v1 to v2, start a session, and the delegated pricing thread runs v2 directly; upgrade to v3 after session creation but before messaging, and the thread still uses v3. In other words, editing a sub-agent does not require updating the coordinator. Conversely, for a "frozen, reproducible team", write version explicitly on the member; the platform accepts and echoes it (GET on the coordinator shows version: N on the member). This is dual to session version locking in Advanced: Prompt Versioning and Rollback: the session's agent field locks the coordinator's own snapshot, the roster's version locks the member reference; omit both and both take latest.

Start the session (with the 9 file resources)

mount_path must start with /uploads/; the actual sandbox path is /mnt/session + mount_path. The 7 cases mount at /uploads/case_studies/<slug>.md, the other two at the /uploads/ root. Instruction texts use actual paths /mnt/session/uploads/....
curl -X POST "$AGENTSTUDIO_URL/sessions" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "'"$AGENT_ID"'",
    "environment_id": "'"$ENV_ID"'",
    "title": "Meridian Health proposal",
    "resources": [
      { "type": "file", "file_id": "<file_st_clair>",   "mount_path": "/uploads/case_studies/st_clair_health.md" },
      { "type": "file", "file_id": "<file_blueridge>",  "mount_path": "/uploads/case_studies/blueridge_health_plan.md" },
      { "type": "file", "file_id": "<file_calder>",     "mount_path": "/uploads/case_studies/calder_manufacturing.md" },
      { "type": "file", "file_id": "<file_northwind>",  "mount_path": "/uploads/case_studies/northwind_logistics.md" },
      { "type": "file", "file_id": "<file_harborview>", "mount_path": "/uploads/case_studies/harborview_retail.md" },
      { "type": "file", "file_id": "<file_aperture>",   "mount_path": "/uploads/case_studies/aperture_payments.md" },
      { "type": "file", "file_id": "<file_summit>",     "mount_path": "/uploads/case_studies/summit_county_gov.md" },
      { "type": "file", "file_id": "<file_product>",    "mount_path": "/uploads/product_one_pager.md" },
      { "type": "file", "file_id": "<file_pricing>",    "mount_path": "/uploads/pricing_rules.md" }
    ]
  }'
The field is agent (an object with just id locks the current latest snapshot; to lock an older one pass {"id": ..., "version": N}). The response's agent echoes the full configuration snapshot (system and tools, but not multiagent — the roster does not enter the session snapshot; member versions resolve at delegation per the table above). Also note: the resources[ ] file_id values in the response differ from what you submitted — the platform generates session-scoped file IDs at mount; the original upload IDs remain valid. The response id looks like sesn_<ULID>, saved as $SESSION_ID.

Start the proposal and observe

Session creation takes no initial input, so the flow is create the session, then post one message event to start work.
curl -X POST "$AGENTSTUDIO_URL/sessions/$SESSION_ID/events" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": [
      { "type": "message", "role": "user",
        "content": [ { "type": "text", "text": "Build a proposal for Meridian Health, a regional healthcare system with ~8500 employees. Estimate 600 seats at heavy usage. Write to /mnt/session/outputs/proposal.md." } ] }
    ]
  }'
Prospect profile: Meridian Health, regional healthcare system, 8,500 employees, estimated 600 seats, heavy usage. The whole run takes about 4 minutes: the coordinator delegates the researcher first (6 web searches), then after the researcher returns delegates the picker (glob + read×N over the library) and the modeler (read the rules) in parallel, then the checker, then reads the one-pager and writes proposal.md (4 drafts — the checker's suggestions drove the rewrites), and mark_artifacts registers the artifact.

Streaming view (one session-level stream, split by thread_id)

SSE is a single session-level stream — the coordinator and all sub-agents converge into the same stream. There is no thread-level SSE endpoint: GET /sessions/{sid}/threads/{tid}/events/stream returns 404 {"type":"api_error","message":"not support"}. To stream, there is only the session stream; to watch a single thread, poll the Thread Events API below.
curl -N "$AGENTSTUDIO_URL/sessions/$SESSION_ID/events/stream" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Accept: text/event-stream"
First comes :connected, then event: message frames (with :keepalive interleaved). Every event carries a top-level thread_id — the split key: the coordinator's main thread is thrd_-prefixed (primary thread); each delegated sub-agent is an sthr_-prefixed session thread. The four multi-agent-specific event types:
  • thread_created: a sub-thread appears. The thread_id is the new sthr_...; content[0].data is {"agent_name": "prospect_researcher", "session_thread_id": "sthr_..."}.
  • thread_message_sent: a message leaves a thread. The sender's thread_id (thrd_ for the coordinator, sthr_ for workers), content[ ].text is the message body, metadata.to_session_thread_id / to_agent_name point at the recipient.
  • thread_message_received: the same message from the recipient's perspective. thread_id is the receiving thread; metadata.from_session_thread_id / from_agent_name identify the sender (messages from the coordinator show from_agent_name: "coordinator").
  • thread_status: thread state transitions. content[0].data is {"agent_name": ..., "thread_status": "running"|"idle", ...}, ending with stop_reason: {"type": "end_turn"}.
Regular events (tool_call / tool_call_output / mcp_call / mcp_call_output / model_request_start|end / reasoning / message / session_status) are as in the getting-started page, just all carrying thread_id. The coordinator's auto-injected orchestration tools appear in the stream as follows:
// Coordinator delegation (on the main thrd_ thread)
{ "type": "tool_call", "thread_id": "thrd_...",
  "content": [ { "type": "data", "data": {
      "name": "create_agent",
      "arguments": { "name": "prospect_researcher", "task": "Research what a regional ..." } } } ] }
// Coordinator polling and querying
{ "name": "wait_for_agents", "arguments": {} }
{ "name": "list_agents",     "arguments": {} }
// Worker side (on an sthr_ thread), formally submitting a result
{ "name": "submit_result", "arguments": { "content": "{\"picks\":[...]}" } }
// submit_result output
{ "output": "{\"status\":\"submitted\"}" }
The worker's return channel: the worker's final assistant text is automatically returned to the coordinator as thread_message_sent; submit_result is a separate explicit submission tool — the two may each arrive in sequence. Sub-agent system prompts need no "return via send_to_parent" phrasing — no tool by that name exists; writing it may only make the model hesitate. Just say "return strict JSON".
Without a long connection, poll the history instead: GET /sessions/$SESSION_ID/events?order=asc. Note the 100-event page cap (a larger limit still returns 100); page with the response's next_page cursor: ...&page=<next_page>. A full multi-agent turn has 168 events — two pages.

The Session Threads API: one row per sub-agent

Structured observation does not require the event stream — Session Threads offers four endpoints for thread-level list, detail, events, and archive (this API group is not yet in the official public docs): List: GET /sessions/{session_id}/threads
curl "$AGENTSTUDIO_URL/sessions/$SESSION_ID/threads" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY"
{
  "data": [
    {
      "id": "sthr_xxx",
      "type": "session_thread",
      "session_id": "sesn_xxx",
      "parent_thread_id": "thrd_xxx",
      "agent": { "id": "agent_xxx", "version": 1 },
      "status": "running",
      "created_at": "2026-09-01T15:00:39Z",
      "updated_at": "2026-09-01T15:01:00Z",
      "archived_at": null
    }
  ],
  "request_id": "req_..."
}
Each thread corresponds to one delegated sub-agent instance. Key fields:
  • parent_thread_id: the delegator's thread. Under the coordinator's main thread it is thrd_...; if a worker delegates its own sub-agents, that thread's parent is the worker's sthr_... (multi-level delegation).
  • agent.version: the member version this thread actually ran — the "omit version, take latest at delegation" rule from the table above is directly visible here. If the roster does not lock a version and the sub-agent upgraded to v2 mid-flight, new sessions show v2 here.
  • status: running / idle / terminated. Polling this endpoint during a batch is a live sub-agent workbench.
Query params limit / page match the other list endpoints; archived threads are excluded by default. Get: GET /sessions/{session_id}/threads/{thread_id} — single-thread detail with the same fields (plus request_id). Two boundaries: the coordinator's main thread (thrd_-prefixed) is not in the threads collection — GET with its id returns 400 invalid_parameter "Invalid request"; the threads resource addresses only delegated sub-threads, the main thread being just a thread_id value on the event stream. Archived threads return the same 400 (absent from List too) — archiving removes a thread from the addressable space, but its events remain queryable (below). Events: GET /sessions/{session_id}/threads/{thread_id}/events — the single thread's event sequence, a thread_id-sliced piece of the session stream: from thread_created (task received) through thread_status (idle + stop_reason), with its own model and tool calls in between. Without limit it returns the thread's full events; with limit the response carries a next_page cursor. This is the shortest path to "what did this one sub-agent do" — no filtering 168 session events by thread_id. Archive: POST /sessions/{session_id}/threads/{thread_id}/archive
curl -X POST "$AGENTSTUDIO_URL/sessions/$SESSION_ID/threads/sthr_xxx/archive" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
The body is optional. Success returns the archived thread object (archived_at filled, updated_at refreshed); List no longer includes it; repeat archiving is idempotent. Use it to clear historical worker threads in long sessions, keeping the list to active ones — event data is unaffected.

What each specialist returned

Walking the four sub-threads' thread_message_sent (or the submit_result arguments.content) via Thread Events yields each specialist's raw JSON:
  • researcher: priorities / pain_points / sources — external intelligence (6 bailian_web_search calls, visible as mcp_call / mcp_call_output with server_label: "WebSearch").
  • picker: picks — glob the directory, read all cases, score, pick two (typical: St. Clair Health + Aperture Payments, healthcare-first, exactly right).
  • modeler: options — reads the rules file and computes for 600 seats × heavy 1.30x (conservative tier $486,720 = 52 × 1.3 × 600 × 12, matching the rules file to the dollar).
  • checker: aligned / issues / suggestions — one alignment verdict plus suggestions.
The three reports differ wildly — external intelligence, local case scoring, pure rule computation, an alignment verdict. Each sub-agent uses only its own tools in its own thread — direct evidence of role-scoped capability.

Client: render one stream into a team workbench

The client-side core is one data structure — a bucket dictionary keyed by thread_id — plus four behaviors that make rendering simpler than expected:
  1. Deduplicate by event id. SSE offers no cursor or resume token; a reconnect may replay old frames — dedupe by id.
  2. Assistant text arrives whole; no incremental assembly. Streaming text lands in one completed frame, with sequence_number typically null and no multi-frame deltas; parse as "accumulate if sequence_number exists, otherwise treat as complete". The live feel comes from structural events: thread_created (who started), thread_status (who finished), mcp_call / tool_call (who is doing what).
  3. session_status is the only session-level event. The turn's only 2 events without thread_id were it (running, and idle+stop_reason) — treat it as session state, belonging to no thread.
  4. The main thread has no thread_created. thrd_ exists before the stream starts — register the first thrd_ event you see as the coordinator.
Put together, a ~50-line team workbench:
#!/usr/bin/env python3
# watch_team.py — render one session-level stream as a team workbench
# usage: curl -N "$AGENTSTUDIO_URL/sessions/$SESSION_ID/events/stream" \
#          -H "Authorization: Bearer $DASHSCOPE_API_KEY" -H "Accept: text/event-stream" \
#        | python3 watch_team.py
import json, sys

threads = {}   # thread_id -> {name, status, last}
seen = set()   # event id dedup (a reconnected stream may replay old frames)

def feed(line):
    d = json.loads(line)
    if d.get("id") in seen:
        return
    seen.add(d.get("id"))
    t, typ = d.get("thread_id"), d.get("type")      # session_status has no thread_id

    if typ == "session_status":                      # session-level event
        s = (d.get("content") or [{}])[0].get("data", {}).get("session_status")
        print(f"=== session {s} ===")
        return
    if typ == "thread_created":                      # sub-thread birth, carries agent_name
        name = (d.get("content") or [{}])[0].get("data", {}).get("agent_name")
        threads[t] = {"name": name, "status": "running", "last": ""}
        print(f"[{name}] started")
        return
    if t not in threads:                             # main thread has no thread_created
        if not (t and t.startswith("thrd_")):
            return
        threads[t] = {"name": "coordinator", "status": "running", "last": ""}
    if typ == "thread_status":
        s = (d.get("content") or [{}])[0].get("data", {}).get("thread_status")
        threads[t]["status"] = s
        print(f"[{threads[t]['name']}] {s}")
    elif typ in ("tool_call", "mcp_call"):           # the main source of liveness
        data = (d.get("content") or [{}])[0].get("data", {})
        print(f"[{threads[t]['name']}] {data.get('name', typ)}")
    elif typ == "message" and d.get("role") == "assistant":
        threads[t]["last"] = "".join(c.get("text", "") for c in d.get("content") or [ ])
        print(f"[{threads[t]['name']}] {threads[t]['last'][:80]!r}")

for ln in sys.stdin:                                 # one data:{...} line per frame
    ln = ln.rstrip("\n")
    if ln.startswith("data:"):
        feed(ln[5:])
Against this turn's stream, it prints a team timeline (excerpt):
=== session running ===
[coordinator] '好的,我来按流程协调团队为 Meridian Health 构建销售提案。…'
[prospect_researcher] started
[coordinator] create_agent
[prospect_researcher] bailian_web_search        ×6
[prospect_researcher] submit_result
[prospect_researcher] idle
[case_study_picker] started
[pricing_modeler] started
[case_study_picker] glob / read
[pricing_modeler] read
[proposal_checker] started
[coordinator] write / write / write / write       ← the checker's suggestions drove 4 rewrites
[coordinator] mark_artifacts
=== session idle ===
Disconnects and the authoritative record: the stream does not replay — frames produced before the connection existed are never re-sent, reconnect included. Treat GET /sessions/{id}/events as authoritative (order=asc + next_page paging, 100 per page, 168 events = 2 pages); when only a few threads matter, align per-thread with GET .../threads/{tid}/events instead. Choosing the observation surface:
WantUse
Whole-team live progress (one connection)Session-level SSE + the bucket dictionary
A "who runs / who finished" workbenchPoll GET /sessions/{id}/threads (status + agent.version)
Dig into one sub-agentGET .../threads/{tid}/events (full single-thread)
Authoritative record / post-mortemGET /sessions/{id}/events, page through everything

Read the proposal — and a failure post-mortem

Retrieving the artifact: find the write tool_call for proposal.md in the coordinator's main-thread events (tool write, path /mnt/session/outputs/proposal.md, arguments.content is the full text — this run also registered it with mark_artifacts, so the file_id is immediately available), or find it by name in the Files list.
The correct scope filter for the Files list is GET /files?scope_type=session&scope_id=<sesn_...> — passing session_id instead is silently ignored and returns the whole workspace's list (distinguish ownership per item by the scope field).
A failure case worth noting: the same coordinator configuration ran twice —
  • Run 1: all three workers correct, but the coordinator's proposal.md contained case names that do not exist in the library — "Community Health Network", "Bayshore Medical Group" — and a pricing table of $39,000/$58,500/$78,000 — the coordinator hallucinated while synthesizing the long context.
  • Run 2: same configuration, same input; proposal.md contained the correct St. Clair Health + Aperture Payments.
The only difference between runs was sampling randomness. Three lessons:
  1. Correct workers ≠ correct team. After the architecture splits research/picking/pricing among specialists, the last mile of synthesis-writing remains a high-risk hallucination zone — the coordinator deserves the strongest model, and a prompt demanding "quote the workers' returned data verbatim; never rewrite numbers" helps.
  2. Attribution via the Threads API. When things go wrong, GET /threads/{id}/events per thread: the picker's picks were right, the modeler's options were right, the error was in the coordinator's write arguments — the problem is pinpointed to the coordinator. A single agent leaves one undifferentiated log; multi-agent keeps an independent record per role, which makes attribution easier.
  3. Evaluation belongs in the pipeline. The methodology of Advanced: Prompt Versioning and Rollback applies here too: before going live, run a fixed prospect profile and assert on proposal.md (case names must be in the library whitelist; pricing must match the rules sheet's computation) before releasing.

Why three specialists (+ one checker) instead of one agent

A single agent with all the tools could write the proposal too, but role-scoped tooling pays concretely:
  • The modeler has only the rules file and no web path — it cannot pull competitor prices; pricing always derives from internal rules.
  • The picker reads 7 files here, hundreds in production — that volume stays in the sub-agent's context, not the coordinator's.
  • The coordinator decides only sequence and handoffs and does no specialist work, keeping its context clean.
Adding proposal_checker extends the same idea: before writing, it pulls cases and pricing back onto the prospect's priorities, catching misalignment pre-write — here it drove 4 rewrites. The check runs in its own thread; the verdict and suggestions stay in the event log for review. The cost is one more worker thread — one more delegation round-trip and some runtime overhead. If a prospect scenario does not need the self-check, drop it from the roster; the coordinator proceeds straight to writing.

Wrap up (archive)

Use archive throughout (audit trail kept, metering stopped). Mind the order: a running session cannot be DELETEd (400 invalid_parameter) — wait for idle or archive; agents are archive-only:
curl -X POST "$AGENTSTUDIO_URL/sessions/$SESSION_ID/archive" -H "Authorization: Bearer $DASHSCOPE_API_KEY"
# or delete: curl -X DELETE "$AGENTSTUDIO_URL/sessions/$SESSION_ID" -H "Authorization: Bearer $DASHSCOPE_API_KEY" (must be idle)
curl -X POST "$AGENTSTUDIO_URL/environments/$ENV_ID/archive"  -H "Authorization: Bearer $DASHSCOPE_API_KEY"
# archive the coordinator and each sub-agent
curl -X POST "$AGENTSTUDIO_URL/agents/$AGENT_ID/archive"      -H "Authorization: Bearer $DASHSCOPE_API_KEY"
Archived sub-agents can no longer be referenced into a new coordinator's roster; historical threads of existing sessions are unaffected (event data persists with the session).

Next steps

Agent Context
Agent Governance & Evolution
Store
Application Marketplace