mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
fix: reject oversized artifacts and split multi_turn into per-verb caps
Two review follow-ups on the agent command tree. Artifact download (data integrity): fetchArtifactURL now reads one byte past maxArtifactBytes and refuses a body over the cap with a typed error, instead of letting io.LimitReader silently truncate a >256 MiB artifact to a corrupt, partial file that reported success (exit 0). The LimitEnforced test is inverted to assert the rejection. Capabilities: the single multi_turn card bit is replaced by three independent capabilities — context_list / context_get / context_delete — each derived from its own wired hook (ListContexts / GetContext / DeleteContext). One bit could not honestly represent three separately-deliverable verbs: a provider wiring ListContexts but not DeleteContext advertised multi_turn=true while `context delete` failed claiming multi_turn=false. Each context verb now gates on and reports its own capability. Card schema, capability matrix, the three context gates, tests, and the lark-agent skill docs are updated in lockstep.
This commit is contained in:
@@ -59,10 +59,10 @@ func TestCapabilityMatrixDiverges(t *testing.T) {
|
||||
if ec.ArtifactDownload || ec.FileInput || ec.TaskCancel {
|
||||
t.Errorf("echo should be the minimal set (no artifact/file/cancel), got %+v", ec)
|
||||
}
|
||||
if !ec.MultiTurn || !ec.TaskGet || !ec.TaskList {
|
||||
t.Errorf("echo should support multi_turn/task_get/task_list, got %+v", ec)
|
||||
if !ec.ContextList || !ec.ContextGet || !ec.ContextDelete || !ec.TaskGet || !ec.TaskList {
|
||||
t.Errorf("echo should support context_list/get/delete + task_get/task_list, got %+v", ec)
|
||||
}
|
||||
if !(rc.ArtifactDownload && rc.FileInput && rc.TaskCancel && rc.InputRequired && rc.MultiTurn && rc.TaskGet && rc.TaskList) {
|
||||
if !(rc.ArtifactDownload && rc.FileInput && rc.TaskCancel && rc.InputRequired && rc.ContextList && rc.ContextGet && rc.ContextDelete && rc.TaskGet && rc.TaskList) {
|
||||
t.Errorf("reporter should have everything enabled, got %+v", rc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,9 +139,11 @@ func printCardPretty(w io.Writer, card *iagent.AgentCard) {
|
||||
// matching the sorted output of the earlier map-based representation.
|
||||
for _, k := range []string{
|
||||
iagent.CapArtifactDownload,
|
||||
iagent.CapContextDelete,
|
||||
iagent.CapContextGet,
|
||||
iagent.CapContextList,
|
||||
iagent.CapFileInput,
|
||||
iagent.CapInputRequired,
|
||||
iagent.CapMultiTurn,
|
||||
iagent.CapTaskCancel,
|
||||
iagent.CapTaskGet,
|
||||
iagent.CapTaskList,
|
||||
|
||||
@@ -29,8 +29,8 @@ func cardTestOpts(t *testing.T, ref string) (*cardOptions, *core.CliConfig) {
|
||||
|
||||
// TestAgentCardRun_ExampleStaticCard verifies that `agent card example:echo`
|
||||
// returns the statically synthesized capability card (no API), with
|
||||
// task_cancel gated off and multi_turn on, and the agent_id echoed from the
|
||||
// ref.
|
||||
// task_cancel gated off and the three context_* caps on, and the agent_id
|
||||
// echoed from the ref.
|
||||
func TestAgentCardRun_ExampleStaticCard(t *testing.T) {
|
||||
opts, _ := cardTestOpts(t, "example:echo")
|
||||
out := opts.Factory.IOStreams.Out.(interface{ Bytes() []byte })
|
||||
@@ -67,8 +67,8 @@ func TestAgentCardRun_ExampleStaticCard(t *testing.T) {
|
||||
if caps["task_cancel"] != false {
|
||||
t.Errorf("echo task_cancel should be false, got %v", caps["task_cancel"])
|
||||
}
|
||||
if caps["multi_turn"] != true {
|
||||
t.Errorf("echo multi_turn should be true, got %v", caps["multi_turn"])
|
||||
if caps["context_list"] != true || caps["context_get"] != true || caps["context_delete"] != true {
|
||||
t.Errorf("echo should support the three context capabilities, got %v", caps)
|
||||
}
|
||||
// parameters / identity must serialize as non-null (guard against omitempty
|
||||
// regression): parameters is always an array (empty [] for example),
|
||||
@@ -110,8 +110,8 @@ func TestAgentCardRun_PrettyFormat(t *testing.T) {
|
||||
if !strings.Contains(text, "echo") {
|
||||
t.Errorf("pretty output should contain agent_id: %s", text)
|
||||
}
|
||||
// multi_turn is a declared capability of the echo card; it must appear.
|
||||
if !strings.Contains(text, "multi_turn") {
|
||||
// context_list is a declared capability of the echo card; it must appear.
|
||||
if !strings.Contains(text, "context_list") {
|
||||
t.Errorf("pretty output should list capabilities: %s", text)
|
||||
}
|
||||
}
|
||||
@@ -183,8 +183,8 @@ func TestPrintCardPretty_AllOptionalFields(t *testing.T) {
|
||||
{Type: "bot", Precondition: "需加入渠道白名单"},
|
||||
},
|
||||
Capabilities: iagent.Capabilities{
|
||||
MultiTurn: true,
|
||||
TaskCancel: false,
|
||||
ContextList: true,
|
||||
TaskCancel: false,
|
||||
},
|
||||
Parameters: []iagent.CardParam{
|
||||
{Name: "locale", Type: "string", Required: true, Desc: "reply locale"},
|
||||
|
||||
@@ -29,7 +29,8 @@ type contextOptions struct {
|
||||
}
|
||||
|
||||
// NewCmdAgentContext builds the `agent context` command group: manage a remote
|
||||
// agent's multi-turn contexts (requires card multi_turn=true). It is a pure group with
|
||||
// agent's multi-turn contexts (each verb gated on its own capability:
|
||||
// context_list / context_get / context_delete). It is a pure group with
|
||||
// no RunE so an unknown subcommand is reported rather than silently swallowed.
|
||||
func NewCmdAgentContext(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
@@ -133,10 +134,10 @@ func agentContextListRun(opts *contextOptions) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Capability gate BEFORE the client: multi_turn is derived from ListContexts
|
||||
// Capability gate BEFORE the client: context_list is derived from ListContexts
|
||||
// being wired, so a spec without it returns unsupported_capability offline.
|
||||
if spec.ListContexts == nil {
|
||||
return capabilityError(opts.Ref, "context list", iagent.CapMultiTurn)
|
||||
return capabilityError(opts.Ref, "context list", iagent.CapContextList)
|
||||
}
|
||||
rt, err := runtimeFor(f, id, agentID)
|
||||
if err != nil {
|
||||
@@ -175,7 +176,7 @@ func agentContextGetRun(opts *contextOptions) error {
|
||||
}
|
||||
// Capability gate BEFORE the client.
|
||||
if spec.GetContext == nil {
|
||||
return capabilityError(opts.Ref, "context get", iagent.CapMultiTurn)
|
||||
return capabilityError(opts.Ref, "context get", iagent.CapContextGet)
|
||||
}
|
||||
rt, err := runtimeFor(f, id, agentID)
|
||||
if err != nil {
|
||||
@@ -214,7 +215,7 @@ func agentContextDeleteRun(opts *contextOptions) error {
|
||||
}
|
||||
// Capability gate BEFORE the client.
|
||||
if spec.DeleteContext == nil {
|
||||
return capabilityError(opts.Ref, "context delete", iagent.CapMultiTurn)
|
||||
return capabilityError(opts.Ref, "context delete", iagent.CapContextDelete)
|
||||
}
|
||||
rt, err := runtimeFor(f, id, agentID)
|
||||
if err != nil {
|
||||
|
||||
@@ -471,9 +471,9 @@ func downloadArtifact(opts *taskOptions) error {
|
||||
}
|
||||
|
||||
// fetchArtifactURL is the production URL fetch: it SSRF-validates rawURL, builds
|
||||
// a download-hardened HTTP client from the Factory and reads at most
|
||||
// maxArtifactBytes of the body. The artifact host is untrusted external content,
|
||||
// so both the URL and the redirect chain are guarded.
|
||||
// a download-hardened HTTP client from the Factory and reads the body up to
|
||||
// maxArtifactBytes, refusing anything larger. The artifact host is untrusted
|
||||
// external content, so both the URL and the redirect chain are guarded.
|
||||
func fetchArtifactURL(ctx context.Context, f *cmdutil.Factory, rawURL string) ([]byte, error) {
|
||||
if err := validate.ValidateDownloadSourceURL(ctx, rawURL); err != nil {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "被拦截的产物 URL: %v", err).
|
||||
@@ -504,9 +504,18 @@ func fetchArtifactURL(ctx context.Context, f *cmdutil.Factory, rawURL string) ([
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errs.NewNetworkError(errs.SubtypeNetworkServer, "下载产物失败: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxArtifactBytes))
|
||||
// Read ONE byte past the cap so an oversized body is detected rather than
|
||||
// silently truncated: io.LimitReader returns EOF (not an error) at the cap, so
|
||||
// reading exactly maxArtifactBytes cannot distinguish "fits" from "overflowed".
|
||||
// A body over the cap is refused with a typed error instead of writing a
|
||||
// corrupt, partial file that would otherwise report success.
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxArtifactBytes+1))
|
||||
if err != nil {
|
||||
return nil, errs.NewNetworkError(errs.SubtypeNetworkTransport, "读取产物响应失败: %v", err).WithCause(err)
|
||||
}
|
||||
if int64(len(data)) > maxArtifactBytes {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"产物超过大小上限 %d 字节,拒绝下载(避免写入被截断的残缺文件)", int64(maxArtifactBytes))
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -847,28 +847,29 @@ func TestFetchArtifactURL_Success(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFetchArtifactURL_LimitEnforced pins the io.LimitReader(maxArtifactBytes)
|
||||
// guard: an oversized body is truncated at exactly maxArtifactBytes so a
|
||||
// hostile host cannot stream an unbounded body onto disk. Uses a streaming
|
||||
// RoundTripper that would otherwise emit far more than the cap.
|
||||
// TestFetchArtifactURL_LimitEnforced pins the size-cap guard: a body larger than
|
||||
// maxArtifactBytes is REJECTED with a typed error rather than silently truncated
|
||||
// onto disk (the fetch reads max+1 to detect the overflow), so a hostile host can
|
||||
// neither stream an unbounded body nor slip a corrupt partial file past as
|
||||
// success. Uses a streaming RoundTripper that emits one byte past the cap.
|
||||
func TestFetchArtifactURL_LimitEnforced(t *testing.T) {
|
||||
restore := swapHardenDownloadClient(passthroughClient)
|
||||
defer restore()
|
||||
|
||||
cfg := &core.CliConfig{AppID: "cli_x", AppSecret: "fake-secret", Brand: core.BrandFeishu}
|
||||
f, _, _, _ := cmdutil.TestFactory(t, cfg)
|
||||
// A body one byte longer than the cap; LimitReader must stop at the cap.
|
||||
// A body one byte longer than the cap must be refused, not truncated.
|
||||
oversized := int64(maxArtifactBytes) + 1
|
||||
f.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: streamRoundTripper{n: oversized, b: 'A'}}, nil
|
||||
}
|
||||
|
||||
got, err := fetchArtifactURL(context.Background(), f, publicArtifactURL)
|
||||
if err != nil {
|
||||
t.Fatalf("an oversized download should not error (should truncate, not fail): %v", err)
|
||||
_, err := fetchArtifactURL(context.Background(), f, publicArtifactURL)
|
||||
if err == nil {
|
||||
t.Fatal("an oversized artifact should be rejected with an error, not truncated to a partial file")
|
||||
}
|
||||
if int64(len(got)) != int64(maxArtifactBytes) {
|
||||
t.Fatalf("downloaded bytes should be truncated to %d, got %d", maxArtifactBytes, len(got))
|
||||
if !errs.IsValidation(err) {
|
||||
t.Fatalf("oversized artifact should be a validation error, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,18 +16,25 @@ const (
|
||||
CapInputRequired = "input_required"
|
||||
CapFileInput = "file_input"
|
||||
CapArtifactDownload = "artifact_download"
|
||||
CapMultiTurn = "multi_turn"
|
||||
// The three multi-turn (context) verbs are independently wired, so each has
|
||||
// its own capability bit — a provider may support listing sessions without
|
||||
// supporting get or delete. (There is no umbrella "multi_turn" bit: a single
|
||||
// flag cannot honestly represent three separately-deliverable hooks.)
|
||||
CapContextList = "context_list"
|
||||
CapContextGet = "context_get"
|
||||
CapContextDelete = "context_delete"
|
||||
)
|
||||
|
||||
// Capabilities is the closed set of capabilities: making it a struct means an
|
||||
// omitted field is an explicit false and a typo is a compile error. Fields are
|
||||
// ordered by json tag alphabetically to keep the key order identical to the old
|
||||
// map serialization.
|
||||
// ordered by json tag alphabetically so the emitted key order is stable.
|
||||
type Capabilities struct {
|
||||
ArtifactDownload bool `json:"artifact_download"`
|
||||
ContextDelete bool `json:"context_delete"`
|
||||
ContextGet bool `json:"context_get"`
|
||||
ContextList bool `json:"context_list"`
|
||||
FileInput bool `json:"file_input"`
|
||||
InputRequired bool `json:"input_required"`
|
||||
MultiTurn bool `json:"multi_turn"`
|
||||
TaskCancel bool `json:"task_cancel"`
|
||||
TaskGet bool `json:"task_get"`
|
||||
TaskList bool `json:"task_list"`
|
||||
@@ -62,7 +69,9 @@ func DeriveCapabilities(s *AgentSpec) Capabilities {
|
||||
TaskList: s.ListTasks != nil,
|
||||
TaskCancel: s.CancelTask != nil,
|
||||
ArtifactDownload: s.DownloadArtifact != nil,
|
||||
MultiTurn: s.ListContexts != nil,
|
||||
ContextList: s.ListContexts != nil,
|
||||
ContextGet: s.GetContext != nil,
|
||||
ContextDelete: s.DeleteContext != nil,
|
||||
FileInput: s.FileInput,
|
||||
InputRequired: s.InputRequired,
|
||||
}
|
||||
@@ -145,8 +154,12 @@ func (c *AgentCard) Supports(capKey string) bool {
|
||||
return c.Capabilities.FileInput
|
||||
case CapInputRequired:
|
||||
return c.Capabilities.InputRequired
|
||||
case CapMultiTurn:
|
||||
return c.Capabilities.MultiTurn
|
||||
case CapContextList:
|
||||
return c.Capabilities.ContextList
|
||||
case CapContextGet:
|
||||
return c.Capabilities.ContextGet
|
||||
case CapContextDelete:
|
||||
return c.Capabilities.ContextDelete
|
||||
case CapTaskCancel:
|
||||
return c.Capabilities.TaskCancel
|
||||
case CapTaskGet:
|
||||
|
||||
@@ -24,32 +24,34 @@ func (fakeRT) CallMultipart(context.Context, string, string, map[string]string,
|
||||
}
|
||||
|
||||
func TestCardSupports(t *testing.T) {
|
||||
c := &AgentCard{Capabilities: Capabilities{TaskCancel: false, MultiTurn: true}}
|
||||
c := &AgentCard{Capabilities: Capabilities{TaskCancel: false, ContextList: true}}
|
||||
if c.Supports(CapTaskCancel) {
|
||||
t.Error("task_cancel should not be supported")
|
||||
}
|
||||
if !c.Supports(CapMultiTurn) {
|
||||
t.Error("multi_turn should be supported")
|
||||
if !c.Supports(CapContextList) {
|
||||
t.Error("context_list should be supported")
|
||||
}
|
||||
if c.Supports("nonexistent") {
|
||||
t.Error("unknown capability should be treated as unsupported")
|
||||
}
|
||||
// nil guard branch: a nil receiver is treated as unsupported; a zero-value Capabilities is all false.
|
||||
var nilCard *AgentCard
|
||||
if nilCard.Supports(CapMultiTurn) {
|
||||
if nilCard.Supports(CapContextList) {
|
||||
t.Error("nil card should be treated as unsupported")
|
||||
}
|
||||
if (&AgentCard{}).Supports(CapMultiTurn) {
|
||||
if (&AgentCard{}).Supports(CapContextList) {
|
||||
t.Error("zero-value Capabilities should be treated as unsupported")
|
||||
}
|
||||
// Each capability constant must map to its own struct field (the switch has no gaps or mismatches).
|
||||
all := &AgentCard{Capabilities: Capabilities{
|
||||
ArtifactDownload: true, FileInput: true, InputRequired: true,
|
||||
MultiTurn: true, TaskCancel: true, TaskGet: true, TaskList: true,
|
||||
ContextList: true, ContextGet: true, ContextDelete: true,
|
||||
TaskCancel: true, TaskGet: true, TaskList: true,
|
||||
}}
|
||||
for _, k := range []string{
|
||||
CapArtifactDownload, CapFileInput, CapInputRequired,
|
||||
CapMultiTurn, CapTaskCancel, CapTaskGet, CapTaskList,
|
||||
CapContextList, CapContextGet, CapContextDelete,
|
||||
CapTaskCancel, CapTaskGet, CapTaskList,
|
||||
} {
|
||||
if !all.Supports(k) {
|
||||
t.Errorf("Supports(%q) should be true when all Capabilities are true", k)
|
||||
@@ -66,10 +68,12 @@ func TestDeriveCapabilities(t *testing.T) {
|
||||
if !c.TaskGet {
|
||||
t.Error("task_get should be true (GetTask is a mandatory core hook)")
|
||||
}
|
||||
if !c.MultiTurn {
|
||||
t.Error("multi_turn should be true (ListContexts wired)")
|
||||
if !c.ContextList {
|
||||
t.Error("context_list should be true (ListContexts wired)")
|
||||
}
|
||||
if c.TaskCancel || c.ArtifactDownload || c.TaskList || c.FileInput || c.InputRequired {
|
||||
// The three context caps are independent: only ListContexts is wired here, so
|
||||
// context_get / context_delete stay false (no umbrella multi_turn bit).
|
||||
if c.TaskCancel || c.ArtifactDownload || c.TaskList || c.FileInput || c.InputRequired || c.ContextGet || c.ContextDelete {
|
||||
t.Errorf("unwired capabilities should be false, got %+v", c)
|
||||
}
|
||||
|
||||
@@ -78,11 +82,13 @@ func TestDeriveCapabilities(t *testing.T) {
|
||||
full.ListTasks = func(context.Context, Runtime, string) ([]TaskSummary, error) { return nil, nil }
|
||||
full.CancelTask = func(context.Context, Runtime, string) error { return nil }
|
||||
full.ListContexts = func(context.Context, Runtime) ([]ContextSummary, error) { return nil, nil }
|
||||
full.GetContext = func(context.Context, Runtime, string) (*ContextDetail, error) { return nil, nil }
|
||||
full.DeleteContext = func(context.Context, Runtime, string) error { return nil }
|
||||
full.DownloadArtifact = func(context.Context, Runtime, string, string) (*ArtifactData, error) { return nil, nil }
|
||||
full.FileInput = true
|
||||
full.InputRequired = true
|
||||
c = DeriveCapabilities(&full)
|
||||
if !(c.TaskGet && c.TaskList && c.TaskCancel && c.MultiTurn && c.ArtifactDownload && c.FileInput && c.InputRequired) {
|
||||
if !(c.TaskGet && c.TaskList && c.TaskCancel && c.ContextList && c.ContextGet && c.ContextDelete && c.ArtifactDownload && c.FileInput && c.InputRequired) {
|
||||
t.Errorf("a fully-wired spec should have every capability true, got %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ metadata:
|
||||
- `submitted` / `working` → 还在跑,稍后再 `task get`(或 `--watch`)
|
||||
- **停轮询条件** = `is_terminal`(∈{completed,failed,canceled,rejected})为真 **或** state ∈ {`input_required`,`auth_required`}(后两者不是错误,是"该你续发了")。
|
||||
- **artifact**:任务产出物(图/文件),列在 `data.artifacts[]`(每项含 `id` + 粗粒度 `kind` 提示);用 `task get --artifact <id> -o <file>` 落盘。选 `-o` 后缀看 `kind`(下载前)与下载输出的 `suggested_name`(下载后,带扩展名);两者仅参考,落盘以 `-o` 为准。
|
||||
- **能力门控**:card `capabilities` 共 7 键(`task_get/task_list/task_cancel/input_required/file_input/artifact_download/multi_turn`),为 false 的动词报 `unsupported_capability`,不静默降级。context 动词无独立键,由 `multi_turn` 伞形覆盖:`multi_turn=false` 时别调 `context list/get/delete`。card 无键的低频能力由运行时兜底——调用报 `unsupported_capability` 与 card 为 false 同样权威,别重试。能力以 `agent card` 实际输出为准;provider 特例见对应 provider 文件。
|
||||
- **能力门控**:card `capabilities` 共 9 键(`task_get/task_list/task_cancel/input_required/file_input/artifact_download/context_list/context_get/context_delete`),为 false 的动词报 `unsupported_capability`,不静默降级。context 三个动词各有独立能力位(`context_list/context_get/context_delete`)——一个 provider 可能能列会话却不能删会话,按需分别判断,别用单一位一概而论。card 无键的低频能力由运行时兜底——调用报 `unsupported_capability` 与 card 为 false 同样权威,别重试。能力以 `agent card` 实际输出为准;provider 特例见对应 provider 文件。
|
||||
|
||||
## 异步与轮询(子进程契约)
|
||||
|
||||
|
||||
@@ -43,9 +43,11 @@ lark-cli agent card <provider>:<agent_id> --jq '.data.capabilities'
|
||||
"description": "把你发的话原样复读一遍(同一会话续发时带轮次,证明上下文记忆)。最小能力集示范。",
|
||||
"capabilities": {
|
||||
"artifact_download": false,
|
||||
"context_delete": true,
|
||||
"context_get": true,
|
||||
"context_list": true,
|
||||
"file_input": false,
|
||||
"input_required": false,
|
||||
"multi_turn": true,
|
||||
"task_cancel": false,
|
||||
"task_get": true,
|
||||
"task_list": true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **前置条件:** 先读 [`../../lark-shared/SKILL.md`](../../lark-shared/SKILL.md)(含高危 exit-10 确认机制)。
|
||||
|
||||
管理远程 agent 的**多轮上下文(会话)**。一个 context(`context_id`)串起同一会话里的多个任务;需 card `multi_turn=true`。续发/追问在 [`agent send --context-id`](lark-agent-send.md),不在此。三个动词都要求该 provider 的全部 scope(all-or-nothing;缺任一即本地报 `missing_scope`,照抄 hint 授权;scope 全集见 provider 文件)。
|
||||
管理远程 agent 的**多轮上下文(会话)**。一个 context(`context_id`)串起同一会话里的多个任务;三个动词各由 `context_list` / `context_get` / `context_delete` 能力位分别门控(provider 可能只支持其中一部分,以 `agent card` 为准)。续发/追问在 [`agent send --context-id`](lark-agent-send.md),不在此。三个动词都要求该 provider 的全部 scope(all-or-nothing;缺任一即本地报 `missing_scope`,照抄 hint 授权;scope 全集见 provider 文件)。
|
||||
|
||||
**分诊心法**:`context list`(哪个会话要处理)→ `context get`(该会话总览 + `active_task`)→ [`agent task list --context-id`](lark-agent-task.md)(该会话全部任务)→ [`agent task get`](lark-agent-task.md)(单任务完整详情)。
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ catalog 型必可枚举,`agent list example` 直接列全部 agent(含 name/
|
||||
|
||||
| capability | `example:echo` | `example:reporter` | 差异含义 |
|
||||
|---|---|---|---|
|
||||
| `task_get` / `task_list` / `multi_turn` | true | true | 两者都支持查任务、列任务、多轮会话 |
|
||||
| `task_get` / `task_list` / `context_list` / `context_get` / `context_delete` | true | true | 两者都支持查/列任务与多轮会话(列/查/删会话)|
|
||||
| `task_cancel` | **false** | true | 对 echo 发 cancel 被命令层门控直接拒(见下方错误样例,不发任何请求);对 reporter 的 cancel 会真正派发(但 mock 任务即时终态,见下方 failed_precondition 样例) |
|
||||
| `file_input` | **false** | true | echo 带 `--file` 报 `unsupported_capability`;reporter 接收附件并在回复里确认 |
|
||||
| `artifact_download` | **false** | true | 只有 reporter 产出 artifact(内联 CSV,`kind=text`,下载输出 `mime=text/csv`、`suggested_name=quarterly_report.csv`) |
|
||||
|
||||
Reference in New Issue
Block a user