@nylorun/harness 0.11.0-beta → 0.12.0-beta

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 4badb5b: Move model execution to session startup, provide Runtime as a mountable Hono router, and generate Hono-first projects with supervised application and Studio development. Studio now resolves root-relative Runtime endpoints correctly for custom mount paths.
8
+
9
+ ## 0.11.1-beta
10
+
11
+ ### Patch Changes
12
+
13
+ - d27242c: Preserve opaque provider continuation metadata through assistant conversation history. Gemini tool calls now retain thought signatures when sending tool results back to the model, including signed empty text and reasoning blocks. Only the originating provider and model receive their signatures.
14
+
3
15
  ## 0.11.0-beta
4
16
 
5
17
  ### Minor Changes
@@ -7,12 +7,11 @@ export interface LoopAgent {
7
7
  readonly invoke: ModelAdapter;
8
8
  }
9
9
  export declare class BuiltAgent {
10
- #private;
11
10
  readonly middleware: readonly BoundMiddleware[];
12
11
  readonly manifest: AgentManifest;
13
12
  readonly id: string;
14
13
  readonly name: string;
15
14
  private constructor();
16
- run(options?: SessionRunOptions): Session;
15
+ run(options: SessionRunOptions): Session;
17
16
  }
18
- export declare function bindAgent(middleware: readonly BoundMiddleware[], invoke: ModelAdapter, manifest: AgentManifest): BuiltAgent;
17
+ export declare function bindAgent(middleware: readonly BoundMiddleware[], manifest: AgentManifest): BuiltAgent;
@@ -7,21 +7,16 @@ export class BuiltAgent {
7
7
  manifest;
8
8
  id;
9
9
  name;
10
- #loopAgent;
11
- constructor(middleware, invoke, manifest) {
10
+ constructor(middleware, manifest) {
12
11
  this.middleware = middleware;
13
12
  this.manifest = manifest;
14
13
  this.id = manifest.id;
15
14
  this.name = manifest.name;
16
- this.#loopAgent = Object.freeze({
17
- middleware,
18
- invoke,
19
- });
20
15
  }
21
16
  static {
22
- createBoundAgent = (middleware, invoke, manifest) => new BuiltAgent(middleware, invoke, manifest);
17
+ createBoundAgent = (middleware, manifest) => new BuiltAgent(middleware, manifest);
23
18
  }
24
- run(options = {}) {
19
+ run(options) {
25
20
  const seeded = "seed" in options && options.seed !== undefined;
26
21
  if (seeded &&
27
22
  (("id" in options && options.id !== undefined) ||
@@ -29,9 +24,9 @@ export class BuiltAgent {
29
24
  ("context" in options && options.context !== undefined)))
30
25
  throw new HarnessError("session.invalid-seed", "Seeded run options cannot include id, userId, or context outside the seed");
31
26
  const id = (seeded ? options.seed.id : "id" in options ? options.id : undefined) ?? createId("session");
32
- return new LiveSession(id, this.#loopAgent, options);
27
+ return new LiveSession(id, Object.freeze({ middleware: this.middleware, invoke: options.onModelCall }), options);
33
28
  }
34
29
  }
35
- export function bindAgent(middleware, invoke, manifest) {
36
- return createBoundAgent(middleware, invoke, manifest);
30
+ export function bindAgent(middleware, manifest) {
31
+ return createBoundAgent(middleware, manifest);
37
32
  }
@@ -1,8 +1,7 @@
1
1
  import type { BuildResult } from "../types/manifest.js";
2
2
  import type { BoundMiddleware } from "../types/middleware.js";
3
- import type { ModelAdapter } from "../types/model.js";
4
3
  import { type BuiltAgent } from "./agent.js";
5
- export declare function assembleAgent(middleware: readonly BoundMiddleware[], invoke: ModelAdapter, identity: Readonly<{
4
+ export declare function assembleAgent(middleware: readonly BoundMiddleware[], identity: Readonly<{
6
5
  id: string;
7
6
  name: string;
8
7
  }>): BuildResult<BuiltAgent>;
@@ -1,7 +1,7 @@
1
1
  import { bindAgent } from "./agent.js";
2
2
  import { createManifest } from "./manifest.js";
3
3
  const diagnostic = (code, message, extra = {}) => Object.freeze({ code, message, ...extra });
4
- export function assembleAgent(middleware, invoke, identity) {
4
+ export function assembleAgent(middleware, identity) {
5
5
  const diagnostics = [];
6
6
  if (typeof identity.id !== "string" || identity.id.length === 0) {
7
7
  diagnostics.push(diagnostic("agent.invalid-id", "Agent id must be a non-empty string"));
@@ -9,9 +9,6 @@ export function assembleAgent(middleware, invoke, identity) {
9
9
  if (typeof identity.name !== "string" || identity.name.length === 0) {
10
10
  diagnostics.push(diagnostic("agent.invalid-name", "Agent name must be a non-empty string"));
11
11
  }
12
- if (typeof invoke !== "function") {
13
- diagnostics.push(diagnostic("harness.invalid-model", "A model invoke function is required"));
14
- }
15
12
  const middlewareIds = new Set();
16
13
  const frozen = [];
17
14
  for (const item of middleware) {
@@ -39,6 +36,6 @@ export function assembleAgent(middleware, invoke, identity) {
39
36
  name: identity.name,
40
37
  middleware: frozenMiddleware,
41
38
  });
42
- const agent = bindAgent(frozenMiddleware, invoke, manifest);
39
+ const agent = bindAgent(frozenMiddleware, manifest);
43
40
  return Object.freeze({ ok: true, agent, manifest });
44
41
  }
@@ -1,5 +1,4 @@
1
1
  import type { BoundMiddleware, CapabilityDeclaration, StepMiddleware } from "../types/middleware.js";
2
- import type { ModelAdapter } from "../types/model.js";
3
2
  import type { BuildDiagnostic } from "../types/shared.js";
4
3
  import { HarnessError } from "../errors.js";
5
4
  import type { BuiltAgent } from "./agent.js";
@@ -12,8 +11,6 @@ interface BuilderState {
12
11
  readonly id: string;
13
12
  readonly name: string;
14
13
  readonly middleware: BoundMiddleware[];
15
- invoke?: ModelAdapter;
16
- bound: boolean;
17
14
  sealed: boolean;
18
15
  agent?: BuiltAgent;
19
16
  error?: AgentBuildError;
@@ -33,14 +30,9 @@ export declare class AgentBuilder {
33
30
  use(middleware: StepMiddleware): this;
34
31
  use(id: string, middleware: StepMiddleware): this;
35
32
  use<State>(declaration: CapabilityDeclaration<State>): this;
36
- with(onModelCall: ModelAdapter): BoundAgentBuilder;
33
+ build(): BuiltAgent;
37
34
  private nextMiddlewareId;
38
35
  private push;
39
36
  private assertOpen;
40
37
  }
41
- export declare class BoundAgentBuilder {
42
- private readonly state;
43
- constructor(state: BuilderState);
44
- build(): BuiltAgent;
45
- }
46
38
  export {};
@@ -30,45 +30,13 @@ export class AgentBuilder {
30
30
  return this.push(compileDeclaration(idOrMiddleware));
31
31
  return this.push({ id: idOrMiddleware, handle: middleware });
32
32
  }
33
- with(onModelCall) {
34
- this.assertOpen("with()");
35
- this.state.bound = true;
36
- this.state.invoke = onModelCall;
37
- return new BoundAgentBuilder(this.state);
38
- }
39
- nextMiddlewareId() {
40
- const taken = new Set(this.state.middleware.map((item) => item.id));
41
- let id;
42
- do {
43
- this.state.middlewareSeq += 1;
44
- id = `middleware-${this.state.middlewareSeq}`;
45
- } while (taken.has(id));
46
- return id;
47
- }
48
- push(entry) {
49
- this.assertOpen("build()");
50
- this.state.middleware.push(entry);
51
- return this;
52
- }
53
- assertOpen(after) {
54
- if (this.state.bound)
55
- throw new AgentLifecycleError("AgentBuilder cannot be changed after with()");
56
- if (this.state.sealed)
57
- throw new AgentLifecycleError(`AgentBuilder cannot be changed after ${after}`);
58
- }
59
- }
60
- export class BoundAgentBuilder {
61
- state;
62
- constructor(state) {
63
- this.state = state;
64
- }
65
33
  build() {
66
34
  if (this.state.agent)
67
35
  return this.state.agent;
68
36
  if (this.state.error)
69
37
  throw this.state.error;
70
38
  this.state.sealed = true;
71
- const result = assembleAgent(this.state.middleware, this.state.invoke, {
39
+ const result = assembleAgent(this.state.middleware, {
72
40
  id: this.state.id,
73
41
  name: this.state.name,
74
42
  });
@@ -79,6 +47,24 @@ export class BoundAgentBuilder {
79
47
  this.state.agent = result.agent;
80
48
  return this.state.agent;
81
49
  }
50
+ nextMiddlewareId() {
51
+ const taken = new Set(this.state.middleware.map((item) => item.id));
52
+ let id;
53
+ do {
54
+ this.state.middlewareSeq += 1;
55
+ id = `middleware-${this.state.middlewareSeq}`;
56
+ } while (taken.has(id));
57
+ return id;
58
+ }
59
+ push(entry) {
60
+ this.assertOpen();
61
+ this.state.middleware.push(entry);
62
+ return this;
63
+ }
64
+ assertOpen() {
65
+ if (this.state.sealed)
66
+ throw new AgentLifecycleError("AgentBuilder cannot be changed after build()");
67
+ }
82
68
  }
83
69
  function createState(options) {
84
70
  const middleware = [];
@@ -90,7 +76,6 @@ function createState(options) {
90
76
  id: options.id,
91
77
  name: options.name,
92
78
  middleware,
93
- bound: false,
94
79
  sealed: false,
95
80
  middlewareSeq: 0,
96
81
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError, BoundAgentBuilder, } from "./build/builder.js";
1
+ export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError } from "./build/builder.js";
2
2
  export type { AgentOptions } from "./build/builder.js";
3
3
  export { HarnessError, isHarnessError } from "./errors.js";
4
4
  export type { HarnessErrorCode, HarnessErrorDetails, HarnessErrorOptions } from "./errors.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError, BoundAgentBuilder, } from "./build/builder.js";
1
+ export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError } from "./build/builder.js";
2
2
  export { HarnessError, isHarnessError } from "./errors.js";
3
3
  export { BuiltAgent } from "./build/agent.js";
4
4
  export { middleware, model, tool } from "./build/helpers.js";
@@ -3,7 +3,7 @@ import { copyJsonObject } from "../utils/immutable.js";
3
3
  import { preparedModel } from "./prepared.js";
4
4
  /** Translate a Harness call to the OpenAI Chat Completions request shape. */
5
5
  export function toChatCompletions(call) {
6
- const messages = call.prompt.map((item) => {
6
+ const messages = withoutProviderReasoning(call.prompt).map((item) => {
7
7
  if (item.kind === "instructions")
8
8
  return { role: "system", content: textOf(item) };
9
9
  if (item.kind === "tool-result")
@@ -93,7 +93,7 @@ export function chatCompletionsAdapter(send) {
93
93
  /** Translate a Harness call to the OpenAI Responses request shape. */
94
94
  export function toResponses(call) {
95
95
  const instructions = call.prompt.filter((item) => item.kind === "instructions").map(textOf);
96
- const input = call.prompt.flatMap((item) => {
96
+ const input = withoutProviderReasoning(call.prompt).flatMap((item) => {
97
97
  if (item.kind === "instructions")
98
98
  return [];
99
99
  if (item.kind === "tool-result")
@@ -201,7 +201,7 @@ export function toMessages(call, defaultMaxOutputTokens) {
201
201
  if (call.outputSchema !== undefined)
202
202
  throw new HarnessError("model.unsupported-output-schema", "Anthropic Messages output schemas require a custom prepared adapter");
203
203
  const instructions = call.prompt.filter((item) => item.kind === "instructions").map(textOf);
204
- const messages = call.prompt.flatMap((item) => {
204
+ const messages = withoutProviderReasoning(call.prompt).flatMap((item) => {
205
205
  if (item.kind === "instructions")
206
206
  return [];
207
207
  if (item.kind === "tool-result")
@@ -543,3 +543,14 @@ function invalidResponse(message, path, cause) {
543
543
  details: { path },
544
544
  });
545
545
  }
546
+ // These portable translators do not interpret another adapter's signed reasoning.
547
+ function withoutProviderReasoning(prompt) {
548
+ return prompt.flatMap((item) => {
549
+ if (item.kind !== "message" ||
550
+ item.role !== "assistant" ||
551
+ !item.content.some((part) => part.type === "reasoning"))
552
+ return [item];
553
+ const content = item.content.filter((part) => part.type !== "reasoning");
554
+ return content.length === 0 ? [] : [{ ...item, content }];
555
+ });
556
+ }
@@ -119,11 +119,11 @@ function normalizeBlock(value, index) {
119
119
  throw invalidCandidate(`Model output[${index}] must be an object`, `output[${index}]`);
120
120
  const block = value;
121
121
  if (block.type === "text" || block.type === "reasoning") {
122
- rejectUnknownKeys(value, ["type", "text"], `Model output[${index}]`);
122
+ rejectUnknownKeys(value, ["type", "text", "providerMetadata"], `Model output[${index}]`);
123
123
  const text = value.text;
124
124
  if (typeof text !== "string")
125
125
  throw invalidCandidate(`Model output[${index}].text must be a string`, `output[${index}].text`);
126
- return Object.freeze({ type: block.type, text });
126
+ return Object.freeze({ type: block.type, text, ...providerMetadata(value, index) });
127
127
  }
128
128
  if (block.type === "json") {
129
129
  rejectUnknownKeys(value, ["type", "value"], `Model output[${index}]`);
@@ -140,7 +140,7 @@ function normalizeBlock(value, index) {
140
140
  }
141
141
  }
142
142
  if (block.type === "tool-call") {
143
- rejectUnknownKeys(value, ["type", "id", "name", "args", "raw"], `Model output[${index}]`);
143
+ rejectUnknownKeys(value, ["type", "id", "name", "args", "raw", "providerMetadata"], `Model output[${index}]`);
144
144
  const raw = value;
145
145
  if (raw.id !== undefined && typeof raw.id !== "string")
146
146
  throw invalidCandidate(`Model output[${index}].id must be a string`, `output[${index}].id`);
@@ -159,6 +159,7 @@ function normalizeBlock(value, index) {
159
159
  throw invalidCandidate(`Model output[${index}].raw must be a string`, `output[${index}].raw`);
160
160
  return Object.freeze({
161
161
  type: "tool-call",
162
+ ...providerMetadata(value, index),
162
163
  id: typeof raw.id === "string" ? raw.id : "",
163
164
  name: typeof raw.name === "string" ? raw.name : "",
164
165
  args,
@@ -240,3 +241,14 @@ function isFiniteNumber(value) {
240
241
  function isNonNegativeInteger(value) {
241
242
  return typeof value === "number" && Number.isInteger(value) && value >= 0;
242
243
  }
244
+ function providerMetadata(value, index) {
245
+ const metadata = value.providerMetadata;
246
+ if (metadata === undefined)
247
+ return {};
248
+ try {
249
+ return { providerMetadata: copyJsonObject(metadata, "providerMetadata") };
250
+ }
251
+ catch (error) {
252
+ throw invalidCandidate(`Model output[${index}].providerMetadata must be a JSON object`, `output[${index}].providerMetadata`, error);
253
+ }
254
+ }
@@ -23,7 +23,6 @@ export declare class SessionScheduler {
23
23
  private generation;
24
24
  private stopPromise?;
25
25
  private recordFailure?;
26
- private readonly observers;
27
26
  private readonly turns;
28
27
  private readonly states;
29
28
  constructor(id: string, agent: LoopAgent, session: Readonly<{
@@ -32,11 +31,12 @@ export declare class SessionScheduler {
32
31
  }>, options?: {
33
32
  readonly seed?: NormalizedSessionSeed;
34
33
  readonly recorder?: SessionRecorder;
34
+ readonly observer?: Observer;
35
35
  });
36
36
  private readonly session;
37
37
  private readonly recorder?;
38
+ private readonly observer?;
38
39
  get snapshot(): SessionSnapshot;
39
- observe(listener: Observer): () => void;
40
40
  submit(event: InputEvent, options?: InputOptions): SubmissionStream;
41
41
  continue(options?: InputOptions): SubmissionStream;
42
42
  private submitWork;
@@ -1,6 +1,6 @@
1
1
  import { HarnessError } from "../errors.js";
2
2
  import { createId } from "../utils/ids.js";
3
- import { createObserverRegistry } from "../utils/observe.js";
3
+ import { emitObserve } from "../utils/observe.js";
4
4
  import { InputQueue, isInteractionReply, snapshotWork, watchInputAbort, } from "./input-queue.js";
5
5
  import { SessionEventLog } from "./event-log.js";
6
6
  import { SubmissionStream } from "./submission-stream.js";
@@ -28,7 +28,6 @@ export class SessionScheduler {
28
28
  generation = 0;
29
29
  stopPromise;
30
30
  recordFailure;
31
- observers = createObserverRegistry();
32
31
  turns;
33
32
  states;
34
33
  constructor(id, agent, session, options = {}) {
@@ -37,6 +36,7 @@ export class SessionScheduler {
37
36
  this.turns = new TurnRunner(agent, id, session);
38
37
  this.session = session;
39
38
  this.recorder = options.recorder;
39
+ this.observer = options.observer;
40
40
  this.states = new CapabilityStateRegistry(agent.middleware, Object.freeze({ id, ...session }), (event) => this.emitObserve(event));
41
41
  if (options.seed) {
42
42
  const event = Object.freeze({
@@ -44,24 +44,15 @@ export class SessionScheduler {
44
44
  revision: options.seed.revision,
45
45
  transcriptEntries: options.seed.transcript.length,
46
46
  });
47
- // Construction precedes public observer registration. Defer this live-only fact
48
- // by one microtask so callers can subscribe immediately after run({ seed }).
49
- queueMicrotask(() => {
50
- if (!this.stopped)
51
- this.emitObserve(event);
52
- });
47
+ this.emitObserve(event);
53
48
  }
54
49
  }
55
50
  session;
56
51
  recorder;
52
+ observer;
57
53
  get snapshot() {
58
54
  return this.snapshotValue;
59
55
  }
60
- observe(listener) {
61
- if (this.stopped)
62
- return () => undefined;
63
- return this.observers.observe(listener);
64
- }
65
56
  submit(event, options) {
66
57
  return this.submitWork(event, options);
67
58
  }
@@ -130,7 +121,6 @@ export class SessionScheduler {
130
121
  }
131
122
  this.emitObserve({ type: "session.stopped", reason });
132
123
  await this.states.shutdown(stopError);
133
- this.observers.clear();
134
124
  })();
135
125
  return this.stopPromise;
136
126
  }
@@ -377,7 +367,7 @@ export class SessionScheduler {
377
367
  }
378
368
  }
379
369
  emitObserve(event) {
380
- this.observers.emit(event);
370
+ emitObserve(this.observer, event);
381
371
  }
382
372
  async commitCancelledPlan(pending, reason) {
383
373
  this.snapshotValue = await this.commit(commitToolResults(this.snapshotValue, pending.turnId, pending.stepId, pending.plan.cancelledResults(reason)), "tool-results");
@@ -246,7 +246,7 @@ function validateSeedCandidate(value, index) {
246
246
  fail(`Candidate output ${blockIndex} at transcript ${index} must be an object`);
247
247
  const block = value;
248
248
  if (block.type === "text" || block.type === "reasoning") {
249
- exactKeys(block, ["type", "text"], `Candidate output ${blockIndex}`);
249
+ exactKeys(block, ["type", "text", "providerMetadata"], `Candidate output ${blockIndex}`);
250
250
  if (typeof block.text !== "string")
251
251
  fail(`Candidate output ${blockIndex} text must be a string`);
252
252
  return;
@@ -257,7 +257,7 @@ function validateSeedCandidate(value, index) {
257
257
  return;
258
258
  }
259
259
  if (block.type === "tool-call") {
260
- exactKeys(block, ["type", "id", "name", "args", "raw"], `Candidate output ${blockIndex}`);
260
+ exactKeys(block, ["type", "id", "name", "args", "raw", "providerMetadata"], `Candidate output ${blockIndex}`);
261
261
  requiredString(block.id, `Candidate output ${blockIndex} id`);
262
262
  requiredString(block.name, `Candidate output ${blockIndex} name`);
263
263
  if (block.raw !== undefined && typeof block.raw !== "string")
@@ -1,6 +1,6 @@
1
1
  import type { InputHandle, InputOptions, MessageInput, Session, SessionEvent, SessionRunOptions, SessionSnapshot, SessionInput } from "../types/session.js";
2
2
  import type { ToolSchemaSource } from "../types/tool.js";
3
- import type { JsonValue, Observer } from "../types/shared.js";
3
+ import type { JsonValue } from "../types/shared.js";
4
4
  import type { LoopAgent } from "../build/agent.js";
5
5
  export declare class LiveSession implements Session {
6
6
  readonly id: string;
@@ -13,6 +13,5 @@ export declare class LiveSession implements Session {
13
13
  interrupt(event: MessageInput, options?: InputOptions): InputHandle<any>;
14
14
  continue(options?: InputOptions): InputHandle<any>;
15
15
  stream(): AsyncIterable<SessionEvent<JsonValue>>;
16
- observe(listener: Observer): () => void;
17
16
  stop(reason?: string): Promise<void>;
18
17
  }
@@ -20,6 +20,7 @@ export class LiveSession {
20
20
  this.scheduler = new SessionScheduler(id, agent, session, {
21
21
  ...(seed === undefined ? {} : { seed }),
22
22
  ...(options.recorder === undefined ? {} : { recorder: options.recorder }),
23
+ ...(options.observer === undefined ? {} : { observer: options.observer }),
23
24
  });
24
25
  }
25
26
  get state() {
@@ -43,9 +44,6 @@ export class LiveSession {
43
44
  stream() {
44
45
  return this.scheduler.events;
45
46
  }
46
- observe(listener) {
47
- return this.scheduler.observe(listener);
48
- }
49
47
  stop(reason) {
50
48
  return this.scheduler.stop(reason);
51
49
  }
@@ -38,6 +38,9 @@ export function canonicalizeOutput(output) {
38
38
  id: call.id,
39
39
  name: call.name,
40
40
  args: call.args,
41
+ ...(block.providerMetadata === undefined
42
+ ? {}
43
+ : { providerMetadata: block.providerMetadata }),
41
44
  ...(block.raw === undefined ? {} : { raw: block.raw }),
42
45
  });
43
46
  })),
@@ -64,8 +64,9 @@ function projectEntry(entry) {
64
64
  }
65
65
  if (entry.kind === "candidate") {
66
66
  const content = entry.candidate.output.flatMap((block) => {
67
- if (block.type === "text")
68
- return [textPart(block.text)];
67
+ if (block.type === "text" ||
68
+ (block.type === "reasoning" && block.providerMetadata !== undefined))
69
+ return [Object.freeze({ ...block })];
69
70
  if (block.type === "json")
70
71
  return [textPart(JSON.stringify(block.value))];
71
72
  if (block.type === "tool-call")
@@ -75,6 +76,9 @@ function projectEntry(entry) {
75
76
  id: block.id,
76
77
  name: block.name,
77
78
  args: copyJson(block.args),
79
+ ...(block.providerMetadata === undefined
80
+ ? {}
81
+ : { providerMetadata: copyJson(block.providerMetadata) }),
78
82
  }),
79
83
  ];
80
84
  return [];
@@ -1,4 +1,5 @@
1
1
  import type { ObserveEmit } from "../utils/observe.js";
2
+ import type { ModelAdapter } from "../types/model.js";
2
3
  import type { InputEvent } from "../types/session.js";
3
4
  import type { ActiveInteractionExecutionRecord, ActiveToolsExecutionRecord } from "../types/session.js";
4
5
  import type { RequiredInteraction, ToolExecutionResume, ToolResult } from "../types/tool.js";
@@ -33,6 +34,7 @@ export interface ToolPlanRunContext {
33
34
  readonly stepId: string;
34
35
  };
35
36
  readonly states?: CapabilityStateRegistry;
37
+ readonly onModelCall: ModelAdapter;
36
38
  }
37
39
  /** Owns one sealed plan's deterministic interaction and concurrent execution progress. */
38
40
  export declare class ToolPlanRunner {
@@ -179,6 +179,7 @@ export class ToolPlanRunner {
179
179
  callId: entry.call.callId,
180
180
  invocationId: entry.invocationId,
181
181
  signal: context.signal,
182
+ onModelCall: context.onModelCall,
182
183
  resume: this.takeResume(entry.call.callId),
183
184
  };
184
185
  if (context.states?.has(entry.owner.middlewareId))
@@ -150,6 +150,7 @@ export class TurnRunner {
150
150
  observe: context.observe,
151
151
  ids: { sessionId: this.sessionId, turnId: pending.turnId, stepId: pending.stepId },
152
152
  states: context.states,
153
+ onModelCall: this.agent.invoke,
153
154
  }, resume);
154
155
  context.assertCurrent();
155
156
  context.onPlanActive(undefined);
@@ -9,14 +9,17 @@ export interface ModelToolCall {
9
9
  export type ModelOutputBlock = {
10
10
  readonly type: "text";
11
11
  readonly text: string;
12
+ readonly providerMetadata?: JsonObject;
12
13
  } | {
13
14
  readonly type: "reasoning";
14
15
  readonly text: string;
16
+ readonly providerMetadata?: JsonObject;
15
17
  } | {
16
18
  readonly type: "json";
17
19
  readonly value: JsonValue;
18
20
  } | {
19
21
  readonly type: "tool-call";
22
+ readonly providerMetadata?: JsonObject;
20
23
  readonly id: string;
21
24
  readonly name: string;
22
25
  readonly args: JsonObject;
@@ -117,14 +120,20 @@ export interface ModelRequest {
117
120
  readonly outputSchema?: JsonObject;
118
121
  }
119
122
  export type PromptContentPart = {
123
+ readonly type: "reasoning";
124
+ readonly text: string;
125
+ readonly providerMetadata?: JsonObject;
126
+ } | {
120
127
  readonly type: "text";
121
128
  readonly text: string;
129
+ readonly providerMetadata?: JsonObject;
122
130
  } | {
123
131
  readonly type: "media";
124
132
  readonly mediaType: string;
125
133
  readonly reference: JsonValue;
126
134
  } | {
127
135
  readonly type: "tool-call";
136
+ readonly providerMetadata?: JsonObject;
128
137
  readonly id: string;
129
138
  readonly name: string;
130
139
  readonly args: JsonObject;
@@ -1,4 +1,4 @@
1
- import type { ModelCall, ModelCandidate } from "./model.js";
1
+ import type { ModelAdapter, ModelCall, ModelCandidate } from "./model.js";
2
2
  import type { JsonObject, JsonValue, Observer, Tripwire } from "./shared.js";
3
3
  import type { RequiredInteraction, SchemaOutput, ToolExecutionResume, ToolOwner, ToolResult, ToolSchemaSource } from "./tool.js";
4
4
  export type InputEvent = {
@@ -70,11 +70,13 @@ export interface InputHandle<Output = string> {
70
70
  readonly completed: Promise<InputCompletion<Output>>;
71
71
  }
72
72
  export interface SessionOptions {
73
+ readonly onModelCall: ModelAdapter;
73
74
  readonly id?: string;
74
75
  readonly userId?: string;
75
76
  readonly context?: JsonObject;
76
77
  readonly seed?: never;
77
78
  readonly recorder?: SessionRecorder;
79
+ readonly observer?: Observer;
78
80
  }
79
81
  /** Stable host-owned facts available while a capability creates session-local state. */
80
82
  export interface SessionIdentity {
@@ -83,8 +85,10 @@ export interface SessionIdentity {
83
85
  readonly context?: JsonObject;
84
86
  }
85
87
  export interface SeededSessionOptions {
88
+ readonly onModelCall: ModelAdapter;
86
89
  readonly seed: SessionSeed;
87
90
  readonly recorder?: SessionRecorder;
91
+ readonly observer?: Observer;
88
92
  readonly id?: never;
89
93
  readonly userId?: never;
90
94
  readonly context?: never;
@@ -222,6 +226,5 @@ export interface Session {
222
226
  interrupt(event: MessageInput, options?: InputOptions): InputHandle;
223
227
  continue(options?: InputOptions): InputHandle;
224
228
  stream(): AsyncIterable<SessionEvent<JsonValue>>;
225
- observe(listener: Observer): () => void;
226
229
  stop(reason?: string): Promise<void>;
227
230
  }
@@ -1,5 +1,6 @@
1
1
  import type { ZodType } from "zod";
2
2
  import type { DeferredOutcome, JsonObject, JsonValue } from "./shared.js";
3
+ import type { ModelAdapter } from "./model.js";
3
4
  export interface SchemaIssue {
4
5
  readonly path: readonly (string | number)[];
5
6
  readonly code: string;
@@ -90,6 +91,8 @@ interface ToolExecutionContextBase {
90
91
  readonly callId: string;
91
92
  readonly invocationId: string;
92
93
  readonly signal: AbortSignal;
94
+ /** The session's model callable, for tools that explicitly run a child agent. */
95
+ readonly onModelCall?: ModelAdapter;
93
96
  readonly resume?: ToolExecutionResume;
94
97
  }
95
98
  export type ToolExecutionContext<State = never> = ToolExecutionContextBase & ([State] extends [never] ? object : {
@@ -1,8 +1,3 @@
1
1
  import type { ObserveEvent, Observer } from "../types/shared.js";
2
2
  export type ObserveEmit = (event: ObserveEvent | (() => ObserveEvent)) => void;
3
- export interface ObserverRegistry {
4
- observe(listener: Observer): () => void;
5
- emit(event: ObserveEvent | (() => ObserveEvent)): void;
6
- clear(): void;
7
- }
8
- export declare function createObserverRegistry(): ObserverRegistry;
3
+ export declare function emitObserve(listener: Observer | undefined, event: ObserveEvent | (() => ObserveEvent)): void;
@@ -1,29 +1,13 @@
1
- export function createObserverRegistry() {
2
- const listeners = new Set();
3
- return {
4
- observe(listener) {
5
- listeners.add(listener);
6
- return () => listeners.delete(listener);
7
- },
8
- emit(event) {
9
- if (listeners.size === 0)
10
- return;
11
- const resolved = typeof event === "function" ? event() : event;
12
- const snapshot = Object.freeze({ ...resolved });
13
- for (const listener of [...listeners]) {
14
- try {
15
- const result = listener(snapshot);
16
- if (result && typeof result.then === "function") {
17
- void Promise.resolve(result).catch(() => undefined);
18
- }
19
- }
20
- catch {
21
- // Observation is deliberately fail-open.
22
- }
23
- }
24
- },
25
- clear() {
26
- listeners.clear();
27
- },
28
- };
1
+ export function emitObserve(listener, event) {
2
+ if (!listener)
3
+ return;
4
+ const snapshot = Object.freeze({ ...(typeof event === "function" ? event() : event) });
5
+ try {
6
+ const result = listener(snapshot);
7
+ if (result && typeof result.then === "function")
8
+ void Promise.resolve(result).catch(() => undefined);
9
+ }
10
+ catch {
11
+ // Observation is deliberately fail-open.
12
+ }
29
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/harness",
3
- "version": "0.11.0-beta",
3
+ "version": "0.12.0-beta",
4
4
  "description": "Nylorun's TypeScript agent runtime. See github.com/nylorun/harness.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -59,11 +59,11 @@
59
59
  "prepack": "npm run build"
60
60
  },
61
61
  "devDependencies": {
62
- "@types/node": "^22.18.0",
62
+ "@types/node": "^26.5.0",
63
63
  "@typescript/native": "npm:typescript@^7.0.2",
64
64
  "prettier": "^3.9.6",
65
65
  "typescript": "npm:@typescript/typescript6@^6.0.2",
66
- "vitest": "^4.1.11",
66
+ "vitest": "^5.0.0",
67
67
  "zod": "^4.1.12"
68
68
  },
69
69
  "peerDependencies": {