@namzu/sdk 40.0.0 → 41.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  3. package/dist/bridge/a2a/mapper.js +8 -0
  4. package/dist/bridge/a2a/mapper.js.map +1 -1
  5. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  6. package/dist/bridge/sse/mapper.js +11 -0
  7. package/dist/bridge/sse/mapper.js.map +1 -1
  8. package/dist/connector/index.d.ts +2 -2
  9. package/dist/connector/index.d.ts.map +1 -1
  10. package/dist/connector/index.js +1 -1
  11. package/dist/connector/index.js.map +1 -1
  12. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  13. package/dist/connector/mcp/adapter.js +92 -4
  14. package/dist/connector/mcp/adapter.js.map +1 -1
  15. package/dist/connector/mcp/audio-admission.d.ts +17 -0
  16. package/dist/connector/mcp/audio-admission.d.ts.map +1 -0
  17. package/dist/connector/mcp/audio-admission.js +171 -0
  18. package/dist/connector/mcp/audio-admission.js.map +1 -0
  19. package/dist/connector/mcp/client.d.ts +252 -1
  20. package/dist/connector/mcp/client.d.ts.map +1 -1
  21. package/dist/connector/mcp/client.js +611 -39
  22. package/dist/connector/mcp/client.js.map +1 -1
  23. package/dist/connector/mcp/envelope.d.ts +91 -0
  24. package/dist/connector/mcp/envelope.d.ts.map +1 -0
  25. package/dist/connector/mcp/envelope.js +173 -0
  26. package/dist/connector/mcp/envelope.js.map +1 -0
  27. package/dist/connector/mcp/era.d.ts +130 -0
  28. package/dist/connector/mcp/era.d.ts.map +1 -0
  29. package/dist/connector/mcp/era.js +304 -0
  30. package/dist/connector/mcp/era.js.map +1 -0
  31. package/dist/connector/mcp/errors.d.ts +106 -0
  32. package/dist/connector/mcp/errors.d.ts.map +1 -0
  33. package/dist/connector/mcp/errors.js +154 -0
  34. package/dist/connector/mcp/errors.js.map +1 -0
  35. package/dist/connector/mcp/http-sse.d.ts +11 -0
  36. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  37. package/dist/connector/mcp/http-sse.js +21 -6
  38. package/dist/connector/mcp/http-sse.js.map +1 -1
  39. package/dist/connector/mcp/index.d.ts +7 -0
  40. package/dist/connector/mcp/index.d.ts.map +1 -1
  41. package/dist/connector/mcp/index.js +10 -0
  42. package/dist/connector/mcp/index.js.map +1 -1
  43. package/dist/connector/mcp/streamable-http.d.ts +83 -0
  44. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  45. package/dist/connector/mcp/streamable-http.js +177 -11
  46. package/dist/connector/mcp/streamable-http.js.map +1 -1
  47. package/dist/connector/mcp/x-mcp-header.d.ts +56 -0
  48. package/dist/connector/mcp/x-mcp-header.d.ts.map +1 -0
  49. package/dist/connector/mcp/x-mcp-header.js +254 -0
  50. package/dist/connector/mcp/x-mcp-header.js.map +1 -0
  51. package/dist/constants/mcp/index.d.ts +123 -15
  52. package/dist/constants/mcp/index.d.ts.map +1 -1
  53. package/dist/constants/mcp/index.js +135 -16
  54. package/dist/constants/mcp/index.js.map +1 -1
  55. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  56. package/dist/manager/agent/lifecycle.js +23 -0
  57. package/dist/manager/agent/lifecycle.js.map +1 -1
  58. package/dist/prompt/coding-agent-doctrine.d.ts +20 -0
  59. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  60. package/dist/prompt/coding-agent-doctrine.js +19 -3
  61. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  62. package/dist/prompt/index.d.ts +1 -1
  63. package/dist/prompt/index.d.ts.map +1 -1
  64. package/dist/prompt/index.js +1 -1
  65. package/dist/prompt/index.js.map +1 -1
  66. package/dist/public-runtime.d.ts +3 -3
  67. package/dist/public-runtime.d.ts.map +1 -1
  68. package/dist/public-runtime.js +3 -3
  69. package/dist/public-runtime.js.map +1 -1
  70. package/dist/sandbox/provider/local.d.ts.map +1 -1
  71. package/dist/sandbox/provider/local.js +46 -3
  72. package/dist/sandbox/provider/local.js.map +1 -1
  73. package/dist/scheduler/local.d.ts.map +1 -1
  74. package/dist/scheduler/local.js +8 -0
  75. package/dist/scheduler/local.js.map +1 -1
  76. package/dist/store/run/disk.d.ts +35 -1
  77. package/dist/store/run/disk.d.ts.map +1 -1
  78. package/dist/store/run/disk.js +100 -0
  79. package/dist/store/run/disk.js.map +1 -1
  80. package/dist/types/agent/scheduler.d.ts +20 -0
  81. package/dist/types/agent/scheduler.d.ts.map +1 -1
  82. package/dist/types/agent/task.d.ts +20 -0
  83. package/dist/types/agent/task.d.ts.map +1 -1
  84. package/dist/types/connector/mcp.d.ts +205 -0
  85. package/dist/types/connector/mcp.d.ts.map +1 -1
  86. package/dist/types/run/events.d.ts +56 -0
  87. package/dist/types/run/events.d.ts.map +1 -1
  88. package/dist/types/run/events.js.map +1 -1
  89. package/dist/types/run/store.d.ts +41 -0
  90. package/dist/types/run/store.d.ts.map +1 -1
  91. package/dist/types/sandbox/index.d.ts +68 -1
  92. package/dist/types/sandbox/index.d.ts.map +1 -1
  93. package/dist/types/sandbox/index.js.map +1 -1
  94. package/package.json +1 -1
  95. package/src/bridge/a2a/mapper.ts +8 -0
  96. package/src/bridge/sse/mapper.ts +11 -0
  97. package/src/connector/index.ts +27 -0
  98. package/src/connector/mcp/adapter.ts +103 -4
  99. package/src/connector/mcp/audio-admission.ts +173 -0
  100. package/src/connector/mcp/client.ts +694 -45
  101. package/src/connector/mcp/envelope.ts +235 -0
  102. package/src/connector/mcp/era.ts +400 -0
  103. package/src/connector/mcp/errors.ts +171 -0
  104. package/src/connector/mcp/http-sse.ts +23 -6
  105. package/src/connector/mcp/index.ts +37 -0
  106. package/src/connector/mcp/streamable-http.ts +199 -11
  107. package/src/connector/mcp/x-mcp-header.ts +322 -0
  108. package/src/constants/mcp/index.ts +145 -16
  109. package/src/manager/agent/lifecycle.ts +29 -0
  110. package/src/prompt/coding-agent-doctrine.ts +31 -4
  111. package/src/prompt/index.ts +1 -0
  112. package/src/public-runtime.ts +28 -0
  113. package/src/sandbox/provider/local.ts +45 -2
  114. package/src/scheduler/local.ts +8 -0
  115. package/src/store/run/disk.ts +108 -0
  116. package/src/types/agent/scheduler.ts +21 -0
  117. package/src/types/agent/task.ts +21 -0
  118. package/src/types/connector/mcp.ts +205 -1
  119. package/src/types/run/events.ts +56 -0
  120. package/src/types/run/store.ts +42 -0
  121. package/src/types/sandbox/index.ts +69 -1
@@ -4,6 +4,7 @@ import {
4
4
  readFile as fsReadFile,
5
5
  writeFile as fsWriteFile,
6
6
  mkdir,
7
+ open,
7
8
  readdir,
8
9
  rename,
9
10
  rm,
@@ -38,6 +39,7 @@ import type {
38
39
  SandboxFileEntry,
39
40
  SandboxIsolationControl,
40
41
  SandboxProvider,
42
+ SandboxReadFileOptions,
41
43
  SandboxSpawnOptions,
42
44
  SandboxStatus,
43
45
  SandboxWalkFilesOptions,
@@ -742,13 +744,54 @@ class LocalSandbox implements Sandbox {
742
744
  this.log.debug('File written', { 'namzu.sandbox.path': resolved })
743
745
  }
744
746
 
745
- async readFile(path: string): Promise<Buffer> {
747
+ /**
748
+ * `options.offset`/`options.length` are HONOURED, not ignored.
749
+ *
750
+ * {@link Sandbox.readFile} is explicit that a backend which takes the
751
+ * parameter and answers with the whole file has given a WRONG answer
752
+ * rather than a degraded one, and must reject instead. On a local
753
+ * filesystem there is nothing to reject: a slice is one positional read,
754
+ * so this serves it.
755
+ *
756
+ * A range that runs past the end returns the bytes that exist, because a
757
+ * caller resuming from a remembered offset cannot know the answer before
758
+ * it asks.
759
+ *
760
+ * `options.signal` cannot be handed to a positional read — `FileHandle`
761
+ * takes none — so it is checked on both sides of one bounded slice
762
+ * instead. That honours the contract's "aborts the read" as far as a
763
+ * local disk allows: an abort is never answered with data.
764
+ */
765
+ async readFile(path: string, options?: SandboxReadFileOptions): Promise<Buffer> {
746
766
  if (this._status === 'destroyed') {
747
767
  throw new Error(`Sandbox ${this.id} is destroyed`)
748
768
  }
749
769
 
750
770
  const resolved = await resolveWithinAnyReal(this.roots, path)
751
- return fsReadFile(resolved)
771
+ const { offset, length, signal } = options ?? {}
772
+ signal?.throwIfAborted()
773
+ if (offset === undefined && length === undefined) {
774
+ return await fsReadFile(resolved, signal ? { signal } : undefined)
775
+ }
776
+ if (offset !== undefined && (!Number.isSafeInteger(offset) || offset < 0)) {
777
+ throw new Error('readFile: offset must be a non-negative safe integer')
778
+ }
779
+ if (length !== undefined && (!Number.isSafeInteger(length) || length < 0)) {
780
+ throw new Error('readFile: length must be a non-negative safe integer')
781
+ }
782
+ const handle = await open(resolved, 'r')
783
+ try {
784
+ const from = offset ?? 0
785
+ const remaining = Math.max(0, (await handle.stat()).size - from)
786
+ const want = Math.min(length ?? remaining, remaining)
787
+ if (want === 0) return Buffer.alloc(0)
788
+ const buf = Buffer.allocUnsafe(want)
789
+ const { bytesRead } = await handle.read(buf, 0, want, from)
790
+ signal?.throwIfAborted()
791
+ return buf.subarray(0, bytesRead)
792
+ } finally {
793
+ await handle.close().catch(() => undefined)
794
+ }
752
795
  }
753
796
 
754
797
  async listFiles(rootPath: string): Promise<readonly SandboxFileEntry[]> {
@@ -114,6 +114,14 @@ export class LocalTaskScheduler implements TaskScheduler {
114
114
  beforeStart: options.beforeStart,
115
115
  ...(options.planId ? { planId: options.planId } : {}),
116
116
  ...(options.planStepId ? { planStepId: options.planStepId } : {}),
117
+ // Display grouping travels with the spawn so the manager can put it
118
+ // on `agent_pending`. Spread conditionally, like the plan edge
119
+ // above: a host that groups nothing must not be made to look like
120
+ // one that grouped everything under an empty label.
121
+ ...(options.workflow ? { workflow: options.workflow } : {}),
122
+ ...(options.phase ? { phase: options.phase } : {}),
123
+ ...(options.phaseDetail ? { phaseDetail: options.phaseDetail } : {}),
124
+ ...(options.phaseOrder !== undefined ? { phaseOrder: options.phaseOrder } : {}),
117
125
  input: {
118
126
  messages: [createUserMessage(options.prompt)],
119
127
  workingDirectory: options.workingDirectory,
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import { appendFile, mkdir, readFile, readdir, stat, unlink } from 'node:fs/promises'
3
3
  import { join } from 'node:path'
4
+ import type { RunExecutionStatus } from '../../types/common/index.js'
4
5
  import type { CheckpointId, IterationCheckpoint } from '../../types/hitl/index.js'
5
6
  import type { Message } from '../../types/message/index.js'
6
7
  import type {
@@ -12,6 +13,7 @@ import type {
12
13
  } from '../../types/run/index.js'
13
14
  import type {
14
15
  CompletedToolRecord,
16
+ DelegatedChildRun,
15
17
  ReadRunEventsOptions,
16
18
  RunMessageSnapshot,
17
19
  RunStore,
@@ -432,6 +434,86 @@ export class RunDiskStore implements RunStore {
432
434
  }
433
435
  }
434
436
 
437
+ /**
438
+ * Every delegated child run saved under one parent, oldest first.
439
+ *
440
+ * The sibling of {@link RunDiskStore.listRuns}, and deliberately not a fix
441
+ * to it. `addToIndex` returns early for any run with a `parentRunId`, which
442
+ * is what keeps delegated children out of `index.json` and therefore out of
443
+ * a host's conversation listing — a child is not a conversation anyone
444
+ * resumes, and putting one there would offer to continue work whose parent
445
+ * turn is long over. That guard stays. This walks the `children/` directory
446
+ * instead, so the evidence a child already wrote is reachable by something
447
+ * that came looking for it, without any of it becoming resumable.
448
+ *
449
+ * READ-ONLY, and that matters more here than for most reads: binding a
450
+ * {@link RunDiskStore} to a run CREATES its directory, so discovery had to
451
+ * be a free walk or it would mint the very directories it claims to find.
452
+ * Nothing here writes, moves or prunes.
453
+ *
454
+ * Tolerant of half-written evidence, the same way the transcript reader
455
+ * next door is. A child directory with no `run.json` — a run killed before
456
+ * its terminal write — is SKIPPED rather than reported with invented
457
+ * fields, and so is one whose `run.json` is not readable JSON. What is
458
+ * skipped is the listing row, not the directory: a caller that knows the
459
+ * run id can still read the transcript beside it.
460
+ *
461
+ * Oldest first, by `startedAt`, so the order matches the order the parent
462
+ * launched them. A child whose `run.json` never recorded a start sorts
463
+ * first; there is no later moment to claim for it.
464
+ *
465
+ * `baseDir` is the runs directory the parent was written under — the same
466
+ * {@link RunStoreConfig.baseDir} the child's store had, which is why one
467
+ * parent's children can be spread across several of them when a host gives
468
+ * each child its own session directory.
469
+ */
470
+ static async listChildren(
471
+ baseDir: string,
472
+ parentRunId: string,
473
+ ): Promise<readonly DelegatedChildRun[]> {
474
+ asRunId(parentRunId)
475
+ const childrenDir = join(baseDir, parentRunId, 'children')
476
+ let names: string[]
477
+ try {
478
+ names = await readdir(childrenDir)
479
+ } catch (err) {
480
+ if (isFileNotFound(err) || isNotADirectory(err)) return []
481
+ throw err
482
+ }
483
+
484
+ const children: DelegatedChildRun[] = []
485
+ for (const name of names) {
486
+ const dir = join(childrenDir, name)
487
+ let meta: unknown
488
+ try {
489
+ meta = JSON.parse(await readFile(join(dir, 'run.json'), 'utf-8'))
490
+ } catch (err) {
491
+ // ENOTDIR covers a stray file sitting beside the child directories.
492
+ if (isFileNotFound(err) || isNotADirectory(err) || err instanceof SyntaxError) continue
493
+ throw err
494
+ }
495
+ if (meta === null || typeof meta !== 'object') continue
496
+ const record = meta as Record<string, unknown>
497
+ const metadata = asRecord(record.metadata)
498
+ const config = asRecord(metadata?.config)
499
+ const usage = asRecord(record.tokenUsage)
500
+ children.push({
501
+ id: name,
502
+ parentRunId,
503
+ dir,
504
+ ...(typeof metadata?.agentId === 'string' ? { agentId: metadata.agentId } : {}),
505
+ ...(typeof metadata?.agentName === 'string' ? { agentName: metadata.agentName } : {}),
506
+ ...(typeof config?.model === 'string' ? { model: config.model } : {}),
507
+ ...(isRunExecutionStatus(record.status) ? { status: record.status } : {}),
508
+ ...(typeof record.startedAt === 'number' ? { startedAt: record.startedAt } : {}),
509
+ ...(typeof record.endedAt === 'number' ? { endedAt: record.endedAt } : {}),
510
+ ...(typeof usage?.totalTokens === 'number' ? { totalTokens: usage.totalTokens } : {}),
511
+ ...(typeof record.depth === 'number' ? { depth: record.depth } : {}),
512
+ })
513
+ }
514
+ return children.sort((left, right) => (left.startedAt ?? 0) - (right.startedAt ?? 0))
515
+ }
516
+
435
517
  async addToIndex(run: Run): Promise<void> {
436
518
  if (run.parentRunId) return
437
519
 
@@ -798,6 +880,32 @@ function parseCheckpoint(content: string, file: string): IterationCheckpoint {
798
880
  return record as IterationCheckpoint
799
881
  }
800
882
 
883
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
884
+ return typeof value === 'object' && value !== null
885
+ ? (value as Record<string, unknown>)
886
+ : undefined
887
+ }
888
+
889
+ const RUN_EXECUTION_STATUSES: readonly RunExecutionStatus[] = [
890
+ 'idle',
891
+ 'pending',
892
+ 'running',
893
+ 'completed',
894
+ 'failed',
895
+ 'cancelled',
896
+ ]
897
+
898
+ function isRunExecutionStatus(value: unknown): value is RunExecutionStatus {
899
+ return typeof value === 'string' && RUN_EXECUTION_STATUSES.includes(value as RunExecutionStatus)
900
+ }
901
+
902
+ /** A path component that is a file where a directory was expected. */
903
+ function isNotADirectory(err: unknown): boolean {
904
+ return (
905
+ typeof err === 'object' && err !== null && (err as NodeJS.ErrnoException).code === 'ENOTDIR'
906
+ )
907
+ }
908
+
801
909
  function isFileNotFound(err: unknown): boolean {
802
910
  return typeof err === 'object' && err !== null && (err as NodeJS.ErrnoException).code === 'ENOENT'
803
911
  }
@@ -58,6 +58,27 @@ export interface CreateTaskOptions {
58
58
  readonly planId?: string
59
59
  readonly planStepId?: string
60
60
 
61
+ /**
62
+ * Display grouping for the delegated child, carried onto its
63
+ * `agent_pending` event so a consumer watching from outside this process
64
+ * can group the child the way this caller meant. Reach, not durability:
65
+ * that event goes straight to a host's listener and enters no run's log,
66
+ * so nothing here is persisted by the kernel. See the `agent_pending`
67
+ * variant in `types/run/events.ts` for the full contract.
68
+ *
69
+ * These fields are display annotations only; they do not create
70
+ * dependencies, barriers, or serial execution. The kernel reads none of
71
+ * them — a caller wanting correlation a host may act on has
72
+ * {@link planId} and {@link planStepId} for that.
73
+ */
74
+ readonly workflow?: string
75
+ /** Stage within {@link workflow}. Display-only on the same terms. */
76
+ readonly phase?: string
77
+ /** Longer text explaining {@link phase}. Display-only on the same terms. */
78
+ readonly phaseDetail?: string
79
+ /** Zero-based DISPLAY order for {@link phase}. Display-only on the same terms. */
80
+ readonly phaseOrder?: number
81
+
61
82
  agentId: string
62
83
 
63
84
  /**
@@ -158,6 +158,27 @@ export interface SendMessageOptions {
158
158
  readonly planId?: string
159
159
  readonly planStepId?: string
160
160
 
161
+ /**
162
+ * Display grouping for the delegated child, carried onto its
163
+ * `agent_pending` event so a consumer watching from outside this process
164
+ * can group the child the way this caller meant. Reach, not durability:
165
+ * that event goes straight to a host's listener and enters no run's log,
166
+ * so nothing here is persisted by the kernel. See the `agent_pending`
167
+ * variant in `types/run/events.ts` for the full contract.
168
+ *
169
+ * These fields are display annotations only; they do not create
170
+ * dependencies, barriers, or serial execution. The kernel reads none of
171
+ * them — a caller wanting correlation a host may act on has
172
+ * {@link planId} and {@link planStepId} for that.
173
+ */
174
+ readonly workflow?: string
175
+ /** Stage within {@link workflow}. Display-only on the same terms. */
176
+ readonly phase?: string
177
+ /** Longer text explaining {@link phase}. Display-only on the same terms. */
178
+ readonly phaseDetail?: string
179
+ /** Zero-based DISPLAY order for {@link phase}. Display-only on the same terms. */
180
+ readonly phaseOrder?: number
181
+
161
182
  agentId: string
162
183
 
163
184
  input: AgentInput
@@ -40,11 +40,40 @@ export interface MCPStdioTransportConfig extends MCPTransportConfigBase {
40
40
  cwd?: string
41
41
  }
42
42
 
43
+ /**
44
+ * Anything that answers like `fetch`, restricted to the request shape the
45
+ * MCP HTTP transports actually send: a URL string, an optional
46
+ * method/headers/body, `redirect` (both transports pin this to `'manual'`
47
+ * so a caller cannot silently re-enable auto-following a redirect), and an
48
+ * abort signal.
49
+ *
50
+ * Structurally identical in spirit to `bridge/a2a/client.ts`'s `FetchLike`
51
+ * — the same injectable, socket-free function shape, so a test needs no
52
+ * socket — but the return type stays the real `Response` rather than that
53
+ * bridge's narrower `{ok, status, json(), text()}` duck type: both MCP
54
+ * transports already read `.headers` (content type, session id) and one of
55
+ * them reads `.body` as a stream (the SSE GET), neither of which the A2A
56
+ * bridge's version exposes. Re-declared here, rather than imported from the
57
+ * A2A bridge, so that bridge is not forced to grow fields it does not use.
58
+ */
59
+ export type MCPFetchLike = (
60
+ input: string,
61
+ init?: {
62
+ method?: string
63
+ headers?: Record<string, string>
64
+ body?: string
65
+ redirect?: 'manual' | 'follow' | 'error'
66
+ signal?: AbortSignal
67
+ },
68
+ ) => Promise<Response>
69
+
43
70
  export interface MCPHttpSseTransportConfig extends MCPTransportConfigBase {
44
71
  type: 'http-sse'
45
72
  url: string
46
73
  headers?: Record<string, string>
47
74
  timeoutMs?: number
75
+ /** Injected in place of the ambient global `fetch`. Defaults to it. */
76
+ fetch?: MCPFetchLike
48
77
  }
49
78
 
50
79
  export interface MCPStreamableHttpTransportConfig extends MCPTransportConfigBase {
@@ -52,6 +81,8 @@ export interface MCPStreamableHttpTransportConfig extends MCPTransportConfigBase
52
81
  url: string
53
82
  headers?: Record<string, string>
54
83
  timeoutMs?: number
84
+ /** Injected in place of the ambient global `fetch`. Defaults to it. */
85
+ fetch?: MCPFetchLike
55
86
  }
56
87
 
57
88
  export type MCPTransportUnion =
@@ -65,6 +96,79 @@ export interface MCPJsonRpcError {
65
96
  data?: unknown
66
97
  }
67
98
 
99
+ /**
100
+ * A protocol revision this client can still negotiate DOWN to when a server
101
+ * does not speak the current spec, newest first.
102
+ *
103
+ * Kept as a literal union (rather than just `string`) so a caller pattern
104
+ * matching on `McpEra` gets real exhaustiveness checking; the runtime array
105
+ * of the same values lives in `constants/mcp` and is typed against this.
106
+ */
107
+ export type McpLegacyVersion = '2025-11-25' | '2025-06-18' | '2025-03-26' | '2024-11-05'
108
+
109
+ /**
110
+ * A protocol revision this client speaks WITHOUT the `initialize`
111
+ * handshake.
112
+ *
113
+ * A modern connection is stateless: there is no handshake, no session id,
114
+ * and every request carries its own protocol version, client capabilities
115
+ * and client info in `_meta`. `connect()` probes for one before it offers
116
+ * the legacy handshake.
117
+ */
118
+ export type McpModernVersion = '2026-07-28'
119
+
120
+ /**
121
+ * Which family of the wire protocol a connection resolved to, and which
122
+ * exact revision within it.
123
+ *
124
+ * `kind` alone tells a caller which rules apply — whether `_meta` and the
125
+ * stateless per-request shape are in play, or the `initialize` handshake
126
+ * and (for 2025-06-18 and later) the `MCP-Protocol-Version` header — without
127
+ * re-deriving it from the version string on every read.
128
+ *
129
+ * `MCPClient.connect()` resolves this by probing for a modern peer first
130
+ * and falling back to the legacy `initialize` handshake, so which arm a
131
+ * given connection lands on is the server's answer, not a configuration.
132
+ */
133
+ export type McpEra =
134
+ | { readonly kind: 'modern'; readonly version: McpModernVersion }
135
+ | { readonly kind: 'legacy'; readonly version: McpLegacyVersion }
136
+
137
+ /**
138
+ * What a modern server answers `server/discover` with.
139
+ *
140
+ * The modern era's replacement for the `initialize` result: it names the
141
+ * revisions the server speaks, what it can do, and — under the reserved
142
+ * `_meta` key — who it is. Every field is optional on the wire as far as
143
+ * this client is concerned, because the one thing it MUST be able to do
144
+ * with a malformed answer is decline to treat it as proof of a modern peer.
145
+ */
146
+ export interface MCPDiscoverResult {
147
+ /** Newest first is conventional but not required; this client sorts. */
148
+ supportedVersions?: readonly string[]
149
+ capabilities?: MCPServerCapabilities
150
+ _meta?: Record<string, unknown>
151
+ }
152
+
153
+ /**
154
+ * Where a resolved {@link McpEra} is remembered between connections.
155
+ *
156
+ * The spec's own guidance: a client SHOULD cache the era for the lifetime
157
+ * of the server process (stdio) or the origin (HTTP) and re-probe if the
158
+ * cached assumption later fails. Without it every connection to a legacy
159
+ * server pays a wasted probe round trip.
160
+ *
161
+ * An interface rather than a module-level `Map` because a process-global
162
+ * cache leaks between tests and would make a conformance suite depend on
163
+ * the order its cases happen to run in. `MCPClientConfig.eraCache` injects
164
+ * one; omitting it uses a process-wide default.
165
+ */
166
+ export interface MCPEraCache {
167
+ get(key: string): McpEra | undefined
168
+ set(key: string, era: McpEra): void
169
+ delete(key: string): void
170
+ }
171
+
68
172
  export interface MCPJsonRpcMessage {
69
173
  jsonrpc: '2.0'
70
174
  id?: string | number
@@ -83,12 +187,48 @@ export interface MCPRequestOptions {
83
187
  * waiting, not that an already-started remote side effect was rolled back.
84
188
  */
85
189
  readonly signal?: AbortSignal
190
+ /**
191
+ * Extra headers for this one request, merged over the transport's static
192
+ * config headers (a collision resolves to this value) and under this same
193
+ * call's `bearerToken`, if both are given.
194
+ *
195
+ * The protocol's own headers are the exception: `MCP-Protocol-Version`,
196
+ * `Mcp-Method` and `Mcp-Name` mirror values inside the request this call
197
+ * is sending, and a server rejects a header that disagrees with the body
198
+ * it mirrors. A value given here under one of those names — matched
199
+ * without regard to case — is refused and warn-logged rather than put on
200
+ * the wire.
201
+ *
202
+ * A transport with no header concept (stdio) receives the field and does
203
+ * nothing with it.
204
+ */
205
+ readonly headers?: Readonly<Record<string, string>>
206
+ /**
207
+ * Sent as `Authorization: Bearer <bearerToken>` on this one request.
208
+ *
209
+ * Overrides a configured `Authorization` header — static or supplied via
210
+ * `headers` above — for this request only; it never touches a
211
+ * differently-named header such as a static `X-API-Key`. Omit it and a
212
+ * configured `Authorization` header is left exactly as configured.
213
+ */
214
+ readonly bearerToken?: string
86
215
  }
87
216
 
88
217
  /** Authority for one transport write and any response body it consumes. */
89
218
  export interface MCPTransportSendOptions {
90
219
  /** A pre-aborted signal starts no transport work. */
91
220
  readonly signal?: AbortSignal
221
+ /**
222
+ * Extra headers for this one send.
223
+ *
224
+ * An HTTP-speaking transport merges these over its static config
225
+ * headers; a transport with no header concept (stdio) receives the
226
+ * field and does nothing with it. Introduced so the client — which
227
+ * alone knows the negotiated era — can ask for `MCP-Protocol-Version`
228
+ * on a post-initialize request without the transport having to know
229
+ * what a protocol version is.
230
+ */
231
+ readonly headers?: Readonly<Record<string, string>>
92
232
  }
93
233
 
94
234
  export interface MCPTransport {
@@ -138,10 +278,38 @@ export interface MCPToolDefinition {
138
278
  annotations?: MCPToolAnnotations
139
279
  }
140
280
 
281
+ /**
282
+ * Audience/priority/freshness hints a server may attach to a content block,
283
+ * part of the schema since 2025-06-18. Advisory only: namzu does not act on
284
+ * any of these fields today, but drops none of them either — they survive
285
+ * into `ToolResult.data` for a host that wants to read them.
286
+ *
287
+ * Distinct from {@link MCPToolAnnotations}, which describes a TOOL
288
+ * (read-only, destructive, …); this describes one piece of CONTENT.
289
+ */
290
+ export interface MCPContentAnnotations {
291
+ audience?: Array<'user' | 'assistant'>
292
+ priority?: number
293
+ lastModified?: string
294
+ }
295
+
141
296
  export type MCPContentBlock =
142
297
  | { type: 'text'; text: string }
143
298
  | { type: 'image'; data: string; mimeType: string }
144
- | { type: 'resource'; resource: { uri: string; mimeType?: string; text?: string } }
299
+ | {
300
+ type: 'resource'
301
+ resource: { uri: string; mimeType?: string; text?: string; blob?: string }
302
+ annotations?: MCPContentAnnotations
303
+ }
304
+ /** Since 2025-03-26. Raw audio bytes, base64-encoded like `image`. */
305
+ | { type: 'audio'; data: string; mimeType: string }
306
+ /**
307
+ * Since 2025-06-18. A pointer to a resource the server has NOT embedded
308
+ * inline — unlike `resource`, which always carries `text` or `blob`.
309
+ * Because this block carries no content at all, the adapter names it
310
+ * for the model rather than fabricating text the server never sent.
311
+ */
312
+ | { type: 'resource_link'; uri: string; name: string; description?: string; mimeType?: string }
145
313
 
146
314
  export interface MCPToolResult {
147
315
  content: MCPContentBlock[]
@@ -160,6 +328,23 @@ export interface MCPToolResult {
160
328
  _meta?: Record<string, unknown>
161
329
  }
162
330
 
331
+ /**
332
+ * One thing the client would have to do that it never declared it could —
333
+ * elicit input, sample a message, list roots, or something a later spec
334
+ * revision defines. Only `method` is read by this client; every other field
335
+ * is carried opaquely so a shape it does not understand still names itself.
336
+ *
337
+ * namzu declares `clientCapabilities: {}` in every era, so MRTR rule 7 — a
338
+ * server MUST NOT send an `inputRequests` entry for a capability the client
339
+ * did not declare — means a CONFORMING server never produces one of these.
340
+ * The type exists for the defensive path: a non-conforming server's demand
341
+ * is named and refused rather than silently misread as an ordinary result.
342
+ */
343
+ export interface MCPInputRequest {
344
+ readonly method: string
345
+ readonly [key: string]: unknown
346
+ }
347
+
163
348
  export interface MCPResource {
164
349
  uri: string
165
350
  name: string
@@ -242,6 +427,25 @@ export interface MCPClientConfig {
242
427
  * forever — no error, no failure, just a run that stopped.
243
428
  */
244
429
  requestTimeoutMs?: number
430
+ /**
431
+ * How long `connect()`'s era probe waits for an answer before deciding
432
+ * the peer speaks a legacy revision. Defaults to
433
+ * `DEFAULT_MCP_ERA_PROBE_TIMEOUT_MS`.
434
+ *
435
+ * Never longer than `requestTimeoutMs`: a probe is a request, and a
436
+ * probe that outlived the deadline every other request is held to would
437
+ * be a connect that hangs past its own timeout.
438
+ */
439
+ eraProbeTimeoutMs?: number
440
+ /**
441
+ * Where this client reads and records the resolved era.
442
+ *
443
+ * Defaults to a process-wide cache shared by every `MCPClient`, which is
444
+ * the point — two clients reaching the same origin should not each pay a
445
+ * probe. Inject a fresh one to isolate a test, or a longer-lived one to
446
+ * scope the memory to a host rather than the process.
447
+ */
448
+ eraCache?: MCPEraCache
245
449
  /**
246
450
  * A pre-built logger. Threaded into the transport `MCPClient` constructs
247
451
  * internally (`createTransport`), so a caller that supplies this gets a
@@ -812,6 +812,62 @@ type CoreRunEvent =
812
812
  /** Approved plan edge carried while the blocking tool is still live. */
813
813
  planId?: string
814
814
  planStepId?: string
815
+ /**
816
+ * How the host that delegated this child wants it GROUPED on screen —
817
+ * a shared label over a set of related delegations, typically one
818
+ * operator-visible piece of work several children are doing together.
819
+ *
820
+ * These fields are display annotations only; they do not create
821
+ * dependencies, barriers, or serial execution. Nothing in the kernel
822
+ * reads them: admission, ordering and concurrency come from the
823
+ * scheduler and from {@link planId}/{@link planStepId}, which is the
824
+ * field pair that DOES carry correlation a host may act on. A reader
825
+ * who infers execution structure from a label here has inferred it
826
+ * from a caption.
827
+ *
828
+ * Absent unless the delegating host supplied them, which is the
829
+ * normal case — a host that groups nothing sends nothing, and a
830
+ * consumer written before these existed reads the same event it
831
+ * always did.
832
+ *
833
+ * They ride this event rather than staying in the delegating
834
+ * process's memory for REACH: a consumer watching from outside
835
+ * that process — another listener, or an SSE client — can rebuild
836
+ * the same picture instead of seeing an undifferentiated list of
837
+ * children.
838
+ *
839
+ * Reach is not durability, and this event buys only the first.
840
+ * Like every delegation lifecycle event, it is handed straight to
841
+ * a host's listener and never enters a run's log — which is what
842
+ * the absent `seq` on this variant says, and what the `seq` doc
843
+ * above spells out. A label here is therefore written nowhere by
844
+ * the kernel and does not survive a restart of the host that chose
845
+ * it; a host wanting the grouping to outlive its process records it
846
+ * from the listener.
847
+ */
848
+ workflow?: string
849
+ /**
850
+ * Display group WITHIN {@link workflow} — a stage of that work, as
851
+ * the delegating host labelled it. Display-only on the same terms as
852
+ * {@link workflow}: it creates no dependencies, barriers or serial
853
+ * execution, and two children naming the same phase are not thereby
854
+ * sequenced or synchronised.
855
+ */
856
+ phase?: string
857
+ /**
858
+ * Longer text explaining {@link phase}, for a surface that has room
859
+ * to show it. Display-only on the same terms as {@link workflow}.
860
+ */
861
+ phaseDetail?: string
862
+ /**
863
+ * Where {@link phase} sits in the host's intended DISPLAY order,
864
+ * zero-based. Display-only on the same terms as {@link workflow}: it
865
+ * orders a list on a screen and orders nothing that runs. Children in
866
+ * one phase are expected to carry the same value; a consumer that
867
+ * sees two disagree should keep the first rather than resequence,
868
+ * because nothing here is authoritative enough to arbitrate.
869
+ */
870
+ phaseOrder?: number
815
871
  }
816
872
  | {
817
873
  type: 'agent_completed'
@@ -28,6 +28,7 @@ import type { RunEvidenceScope, RunTextEvidenceSource } from '../../store/eviden
28
28
  * re-keyed per call, it happens once, deliberately, as its own change.
29
29
  */
30
30
 
31
+ import type { RunExecutionStatus } from '../common/index.js'
31
32
  import type { Message } from '../message/index.js'
32
33
  import type { AuditEvent } from './audit.js'
33
34
  import type { Run } from './entity.js'
@@ -103,6 +104,47 @@ export type ToolExecutionRecord =
103
104
  | (CompletedToolRecord & { readonly status: 'completed' })
104
105
  | { readonly toolUseId: string; readonly toolName: string; readonly status: 'started' }
105
106
 
107
+ /**
108
+ * One delegated child run found on disk under its parent's `children/`
109
+ * directory, as {@link import('../../store/run/disk.js').RunDiskStore.listChildren}
110
+ * reports it.
111
+ *
112
+ * A DISCOVERY record, not the child's evidence: every field here comes from
113
+ * the child's `run.json`, and the transcript, message snapshot and report
114
+ * beside it stay on disk until something asks for them. {@link dir} is what
115
+ * that something reads from.
116
+ *
117
+ * Everything the file supplies is optional, because a `run.json` is written
118
+ * by the child's own terminal path and a process killed before it got there
119
+ * leaves a directory whose other evidence is still worth opening. An absent
120
+ * field is "this file did not say", never a zero or an empty string.
121
+ */
122
+ export interface DelegatedChildRun {
123
+ /**
124
+ * The child's run id, taken from the directory name.
125
+ *
126
+ * The location is the fact: `initRun` names the directory after the run
127
+ * it binds, so a `run.json` whose `id` disagrees with its own directory
128
+ * was moved or hand-edited, and the directory is the half that decides
129
+ * where the evidence actually is.
130
+ */
131
+ readonly id: string
132
+ /** The parent run whose `children/` directory holds this one. */
133
+ readonly parentRunId: string
134
+ /** Absolute path to the child's evidence directory. */
135
+ readonly dir: string
136
+ readonly agentId?: string
137
+ readonly agentName?: string
138
+ /** `metadata.config.model` — the model the child was configured with. */
139
+ readonly model?: string
140
+ readonly status?: RunExecutionStatus
141
+ readonly startedAt?: number
142
+ readonly endedAt?: number
143
+ /** `tokenUsage.totalTokens` — this child's own cumulative spend. */
144
+ readonly totalTokens?: number
145
+ readonly depth?: number
146
+ }
147
+
106
148
  /** Absence proves no recorded start only when the whole selected log is complete. */
107
149
  export interface ToolExecutionSnapshot {
108
150
  readonly complete: boolean