Why: the completion doc requires the client to recognize resultType, treat input_required as a protocol-level result whose answer retries the original tools/call, and expose tasks/get|update|cancel; today only a content-block special case existed and the advertised version was never checked against the server's. Evidence: GOWORK=off go vet + go test -count=1 ./... green; TestInputRequiredResultTypeRetriesOriginalCall (ask → tasks/update → retry, call order and final content), TestNegotiatedVersionFallbackKeepsLegacyPath (byte-level legacy behavior on 2024-11-05), TestTasksGetAndCancelRoundTrip, TestRunEndCancelsSeenTasks, TestDefaultSchemaDeclares202012; the acceptance-③ regression TestStdioSlowCancelThenEcho passes unchanged. Impact: advertise 2026-07-28 and branch on the negotiated version (older servers keep byte-identical behavior); synthesized default tool schemas now declare JSON Schema 2020-12; runs cancel the MCP tasks they saw via a per-run TaskTracker (other runs on the shared manager are unaffected). Refs: Agent-System-Documents#42 (N2 plan WP3)
66 lines
1.6 KiB
Go
66 lines
1.6 KiB
Go
package mcp
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
)
|
||
|
||
// TaskTracker 记录一次 run 内 tools/call 结果里出现过的长任务(按服务端
|
||
// 客户端分组),供 run 结束时对这些任务发 tasks/cancel(2026-07-28 最小
|
||
// 产品映射:取消只覆盖本 run 自己的任务,不影响共享 Manager 上其他 run)。
|
||
type TaskTracker struct {
|
||
mu sync.Mutex
|
||
seen map[Client]map[string]struct{}
|
||
}
|
||
|
||
type trackerKey struct{}
|
||
|
||
// WithTaskTracker 把任务跟踪器放进上下文(bridgeTool 记录 TaskID)。
|
||
func WithTaskTracker(ctx context.Context) (context.Context, *TaskTracker) {
|
||
tr := &TaskTracker{seen: map[Client]map[string]struct{}{}}
|
||
return context.WithValue(ctx, trackerKey{}, tr), tr
|
||
}
|
||
|
||
func trackerFrom(ctx context.Context) *TaskTracker {
|
||
if ctx == nil {
|
||
return nil
|
||
}
|
||
tr, _ := ctx.Value(trackerKey{}).(*TaskTracker)
|
||
return tr
|
||
}
|
||
|
||
func (t *TaskTracker) note(c Client, taskID string) {
|
||
if t == nil || c == nil || taskID == "" {
|
||
return
|
||
}
|
||
t.mu.Lock()
|
||
set := t.seen[c]
|
||
if set == nil {
|
||
set = map[string]struct{}{}
|
||
t.seen[c] = set
|
||
}
|
||
set[taskID] = struct{}{}
|
||
t.mu.Unlock()
|
||
}
|
||
|
||
// CancelAll 对记录的任务逐一向其服务端发 tasks/cancel(尽力而为,
|
||
// 首个错误后继续)。返回实际发出取消的任务数。
|
||
func (t *TaskTracker) CancelAll(ctx context.Context) int {
|
||
if t == nil {
|
||
return 0
|
||
}
|
||
t.mu.Lock()
|
||
seen := t.seen
|
||
t.seen = map[Client]map[string]struct{}{}
|
||
t.mu.Unlock()
|
||
n := 0
|
||
for c, ids := range seen {
|
||
for id := range ids {
|
||
if err := c.TasksCancel(ctx, id); err == nil {
|
||
n++
|
||
}
|
||
}
|
||
}
|
||
return n
|
||
}
|