@nylorun/harness 0.5.0-beta.1

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 (87) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +192 -0
  3. package/README.md +124 -0
  4. package/dist/build/adapters.d.ts +11 -0
  5. package/dist/build/adapters.js +91 -0
  6. package/dist/build/agent.d.ts +19 -0
  7. package/dist/build/agent.js +28 -0
  8. package/dist/build/assemble.d.ts +9 -0
  9. package/dist/build/assemble.js +76 -0
  10. package/dist/build/bind-tool.d.ts +4 -0
  11. package/dist/build/bind-tool.js +15 -0
  12. package/dist/build/builder.d.ts +31 -0
  13. package/dist/build/builder.js +74 -0
  14. package/dist/build/helpers.d.ts +7 -0
  15. package/dist/build/helpers.js +5 -0
  16. package/dist/build/manifest.d.ts +9 -0
  17. package/dist/build/manifest.js +15 -0
  18. package/dist/build/schema.d.ts +19 -0
  19. package/dist/build/schema.js +86 -0
  20. package/dist/errors.d.ts +17 -0
  21. package/dist/errors.js +17 -0
  22. package/dist/index.d.ts +11 -0
  23. package/dist/index.js +4 -0
  24. package/dist/model-normalize.d.ts +6 -0
  25. package/dist/model-normalize.js +211 -0
  26. package/dist/session/event-log.d.ts +11 -0
  27. package/dist/session/event-log.js +63 -0
  28. package/dist/session/input-queue.d.ts +29 -0
  29. package/dist/session/input-queue.js +78 -0
  30. package/dist/session/scheduler.d.ts +48 -0
  31. package/dist/session/scheduler.js +352 -0
  32. package/dist/session/session.d.ts +14 -0
  33. package/dist/session/session.js +55 -0
  34. package/dist/session/state.d.ts +10 -0
  35. package/dist/session/state.js +36 -0
  36. package/dist/session/submission-stream.d.ts +13 -0
  37. package/dist/session/submission-stream.js +36 -0
  38. package/dist/step/canonicalize.d.ts +21 -0
  39. package/dist/step/canonicalize.js +62 -0
  40. package/dist/step/compose.d.ts +4 -0
  41. package/dist/step/compose.js +106 -0
  42. package/dist/step/context-draft.d.ts +10 -0
  43. package/dist/step/context-draft.js +71 -0
  44. package/dist/step/model-configuration.d.ts +15 -0
  45. package/dist/step/model-configuration.js +153 -0
  46. package/dist/step/project.d.ts +2 -0
  47. package/dist/step/project.js +103 -0
  48. package/dist/step/resolve.d.ts +9 -0
  49. package/dist/step/resolve.js +16 -0
  50. package/dist/step/run.d.ts +27 -0
  51. package/dist/step/run.js +127 -0
  52. package/dist/step/seal.d.ts +31 -0
  53. package/dist/step/seal.js +108 -0
  54. package/dist/step/slot-assembly.d.ts +39 -0
  55. package/dist/step/slot-assembly.js +52 -0
  56. package/dist/step/step-context.d.ts +36 -0
  57. package/dist/step/step-context.js +255 -0
  58. package/dist/turn/plan-runner.d.ts +55 -0
  59. package/dist/turn/plan-runner.js +370 -0
  60. package/dist/turn/runner.d.ts +53 -0
  61. package/dist/turn/runner.js +128 -0
  62. package/dist/types/manifest.d.ts +22 -0
  63. package/dist/types/manifest.js +1 -0
  64. package/dist/types/middleware.d.ts +65 -0
  65. package/dist/types/middleware.js +1 -0
  66. package/dist/types/model.d.ts +166 -0
  67. package/dist/types/model.js +1 -0
  68. package/dist/types/session.d.ts +122 -0
  69. package/dist/types/session.js +1 -0
  70. package/dist/types/shared.d.ts +180 -0
  71. package/dist/types/shared.js +1 -0
  72. package/dist/types/tool.d.ts +101 -0
  73. package/dist/types/tool.js +1 -0
  74. package/dist/utils/digest.d.ts +1 -0
  75. package/dist/utils/digest.js +14 -0
  76. package/dist/utils/ids.d.ts +1 -0
  77. package/dist/utils/ids.js +3 -0
  78. package/dist/utils/immutable.d.ts +5 -0
  79. package/dist/utils/immutable.js +54 -0
  80. package/dist/utils/maps.d.ts +1 -0
  81. package/dist/utils/maps.js +37 -0
  82. package/dist/utils/observe.d.ts +8 -0
  83. package/dist/utils/observe.js +29 -0
  84. package/docs/loop.md +47 -0
  85. package/docs/model-call-projection.md +112 -0
  86. package/docs/reference.md +122 -0
  87. package/package.json +70 -0
@@ -0,0 +1,74 @@
1
+ import { HarnessError } from "../errors.js";
2
+ import { assembleAgent } from "./assemble.js";
3
+ export class AgentBuildError extends HarnessError {
4
+ diagnostics;
5
+ constructor(diagnostics) {
6
+ super("agent.build-failed", diagnostics.map((item) => item.message).join("; ") || "Agent build failed");
7
+ this.diagnostics = diagnostics;
8
+ this.name = "AgentBuildError";
9
+ }
10
+ }
11
+ export class AgentLifecycleError extends HarnessError {
12
+ constructor(message) {
13
+ super("agent.lifecycle-sealed", message);
14
+ this.name = "AgentLifecycleError";
15
+ }
16
+ }
17
+ export function Agent(model, directive) {
18
+ return new AgentBuilder(model, directive);
19
+ }
20
+ export class AgentBuilder {
21
+ invoke;
22
+ directive;
23
+ middleware = [];
24
+ adapters = [];
25
+ sealed = false;
26
+ agent;
27
+ error;
28
+ middlewareSeq = 0;
29
+ constructor(invoke, directive) {
30
+ this.invoke = invoke;
31
+ this.directive = directive;
32
+ }
33
+ with(adapter, options) {
34
+ if (this.sealed)
35
+ throw new AgentLifecycleError("AgentBuilder cannot be changed after build()");
36
+ this.adapters.push({ adapter, ...(options === undefined ? {} : { options }) });
37
+ return this;
38
+ }
39
+ use(idOrMiddleware, middleware) {
40
+ if (typeof idOrMiddleware === "function") {
41
+ return this.push({ id: this.nextMiddlewareId(), handle: idOrMiddleware });
42
+ }
43
+ return this.push({ id: idOrMiddleware, handle: middleware });
44
+ }
45
+ build() {
46
+ if (this.agent)
47
+ return this.agent;
48
+ if (this.error)
49
+ throw this.error;
50
+ this.sealed = true;
51
+ const result = assembleAgent(this.middleware, this.invoke, this.adapters, this.directive);
52
+ if (!result.ok) {
53
+ this.error = new AgentBuildError(result.diagnostics);
54
+ throw this.error;
55
+ }
56
+ this.agent = result.agent;
57
+ return this.agent;
58
+ }
59
+ nextMiddlewareId() {
60
+ const taken = new Set(this.middleware.map((item) => item.id));
61
+ let id;
62
+ do {
63
+ this.middlewareSeq += 1;
64
+ id = `middleware-${this.middlewareSeq}`;
65
+ } while (taken.has(id));
66
+ return id;
67
+ }
68
+ push(entry) {
69
+ if (this.sealed)
70
+ throw new AgentLifecycleError("AgentBuilder cannot be changed after build()");
71
+ this.middleware.push(entry);
72
+ return this;
73
+ }
74
+ }
@@ -0,0 +1,7 @@
1
+ import type { ModelAdapter } from "../types/model.js";
2
+ import type { StepMiddleware } from "../types/middleware.js";
3
+ import type { ToolAdapter, ToolDefinition } from "../types/tool.js";
4
+ export declare const tool: <T extends ToolDefinition>(value: T) => T;
5
+ export declare const adapter: <T extends ToolAdapter>(value: T) => T;
6
+ export declare const model: <T extends ModelAdapter>(value: T) => T;
7
+ export declare const middleware: <T extends StepMiddleware>(value: T) => T;
@@ -0,0 +1,5 @@
1
+ import { prepareTool } from "./schema.js";
2
+ export const tool = (value) => prepareTool(value);
3
+ export const adapter = (value) => value;
4
+ export const model = (value) => value;
5
+ export const middleware = (value) => value;
@@ -0,0 +1,9 @@
1
+ import type { AgentManifest } from "../types/manifest.js";
2
+ import type { BoundMiddleware } from "../types/middleware.js";
3
+ import type { ModelDirective } from "../types/model.js";
4
+ import type { AdapterRegistry } from "./adapters.js";
5
+ export declare function createManifest(input: {
6
+ middleware: readonly BoundMiddleware[];
7
+ directive?: ModelDirective;
8
+ adapters: AdapterRegistry;
9
+ }): AgentManifest;
@@ -0,0 +1,15 @@
1
+ import { deepFreeze } from "../utils/immutable.js";
2
+ export function createManifest(input) {
3
+ const middleware = input.middleware.map(({ id }) => ({ id }));
4
+ const adapters = [...input.adapters.entries].map(([, entry]) => ({
5
+ id: entry.adapter.id,
6
+ ...(entry.options.maxConcurrentCalls === undefined
7
+ ? {}
8
+ : { maxConcurrentCalls: entry.options.maxConcurrentCalls }),
9
+ }));
10
+ return deepFreeze({
11
+ middleware,
12
+ ...(input.directive === undefined ? {} : { model: input.directive }),
13
+ adapters,
14
+ });
15
+ }
@@ -0,0 +1,19 @@
1
+ import type { output } from "zod";
2
+ import type { ToolDefinition, ToolObjectSchema } from "../types/tool.js";
3
+ export type SchemaValidation<T> = {
4
+ readonly ok: true;
5
+ readonly value: T;
6
+ } | {
7
+ readonly ok: false;
8
+ readonly issues: readonly string[];
9
+ };
10
+ export interface NormalizedToolSchema<T> {
11
+ readonly jsonSchema: import("../types/shared.js").JsonObject;
12
+ validate(value: unknown): SchemaValidation<T>;
13
+ }
14
+ /** Eagerly prepares a definition authored through Harness's tool() helper. */
15
+ export declare function prepareTool<T extends ToolDefinition>(definition: T): T;
16
+ /** Returns a cached normalized schema, preparing raw definitions on first bind. */
17
+ export declare function normalizedSchemaFor(definition: ToolDefinition): NormalizedToolSchema<unknown>;
18
+ /** Converts a synchronous Zod object schema into Harness's immutable runtime representation. */
19
+ export declare function normalizeSchema<Parameters extends ToolObjectSchema>(parameters: Parameters): NormalizedToolSchema<output<Parameters>>;
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+ import { HarnessError } from "../errors.js";
3
+ import { copyJsonObject } from "../utils/immutable.js";
4
+ const preparedSchemas = new WeakMap();
5
+ /** Eagerly prepares a definition authored through Harness's tool() helper. */
6
+ export function prepareTool(definition) {
7
+ normalizedSchemaFor(definition);
8
+ return definition;
9
+ }
10
+ /** Returns a cached normalized schema, preparing raw definitions on first bind. */
11
+ export function normalizedSchemaFor(definition) {
12
+ const cached = preparedSchemas.get(definition);
13
+ if (cached)
14
+ return cached;
15
+ const normalized = normalizeSchema(definition.parameters);
16
+ preparedSchemas.set(definition, normalized);
17
+ return normalized;
18
+ }
19
+ /** Converts a synchronous Zod object schema into Harness's immutable runtime representation. */
20
+ export function normalizeSchema(parameters) {
21
+ if (!(parameters instanceof z.ZodObject)) {
22
+ throw new HarnessError("tool.invalid-schema", "Tool parameters schema must be a Zod object schema");
23
+ }
24
+ if (hasDeclaredAsyncWork(parameters)) {
25
+ throw new HarnessError("tool.invalid-schema", "Tool parameters schema must validate synchronously");
26
+ }
27
+ let jsonSchema;
28
+ try {
29
+ jsonSchema = copyJsonObject(z.toJSONSchema(parameters, { target: "draft-07" }), "tool.parameters.jsonSchema");
30
+ }
31
+ catch (error) {
32
+ throw new HarnessError("tool.invalid-schema", `Tool parameters schema must convert to JSON Schema: ${message(error)}`, { cause: error });
33
+ }
34
+ if (jsonSchema.type !== "object") {
35
+ throw new HarnessError("tool.invalid-schema", "Tool JSON Schema root type must be object");
36
+ }
37
+ return Object.freeze({
38
+ jsonSchema,
39
+ validate(value) {
40
+ try {
41
+ const result = parameters.safeParse(value);
42
+ if (result.success)
43
+ return { ok: true, value: result.data };
44
+ return {
45
+ ok: false,
46
+ issues: result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`),
47
+ };
48
+ }
49
+ catch (error) {
50
+ return { ok: false, issues: [synchronousIssue(error)] };
51
+ }
52
+ },
53
+ });
54
+ }
55
+ /** Zod exposes declared checks in its stable v4 definition graph; reject async checks before a run. */
56
+ function hasDeclaredAsyncWork(schema) {
57
+ return visitDefinition(schema._zod?.def, new Set());
58
+ }
59
+ function visitDefinition(value, seen) {
60
+ if (typeof value === "function")
61
+ return value.constructor.name === "AsyncFunction";
62
+ if (!value || typeof value !== "object")
63
+ return false;
64
+ if (seen.has(value))
65
+ return false;
66
+ seen.add(value);
67
+ // Nested Zod schemas carry their own definition graph. Inspect it directly so public helpers
68
+ // such as `parseAsync` do not make every otherwise-synchronous schema look asynchronous.
69
+ const nestedDefinition = value._zod?.def;
70
+ if (nestedDefinition !== undefined && nestedDefinition !== value)
71
+ return visitDefinition(nestedDefinition, seen);
72
+ for (const item of Object.values(value)) {
73
+ if (visitDefinition(item, seen))
74
+ return true;
75
+ }
76
+ return false;
77
+ }
78
+ function synchronousIssue(error) {
79
+ const reason = message(error);
80
+ return reason.includes("Promise during synchronous parse")
81
+ ? "Schema validation must be synchronous"
82
+ : reason;
83
+ }
84
+ function message(error) {
85
+ return error instanceof Error ? error.message : String(error);
86
+ }
@@ -0,0 +1,17 @@
1
+ /** Machine-readable error raised by Harness-owned code. */
2
+ export type HarnessErrorCode = "adapter.invalid-outcome" | "adapter.not-registered" | "agent.build-failed" | "agent.lifecycle-sealed" | "context.invalid-item" | "context.invalid-item-type" | "context.invalid-order" | "context.invalid-reason" | "context.invalid-slot" | "interaction.invalid" | "interaction.missing-resume" | "interaction.uncorrelated-resume" | "json.invalid-data" | "json.invalid-object" | "middleware.next-after-return" | "middleware.next-called-twice" | "middleware.request-mutators-revoked" | "model.candidate-missing" | "model.invalid-candidate" | "model.invalid-directive" | "configuration.duplicate-tool-name" | "configuration.invalid" | "configuration.invalid-instructions" | "configuration.invalid-order" | "configuration.invalid-reason" | "configuration.invalid-slot" | "configuration.invalid-tools" | "configuration.model-selection-conflict" | "response.invalid-replacement" | "session.stale-result" | "tool.invalid-arguments" | "tool.invalid-name" | "tool.invalid-schema" | "tool.invalid-tool-result";
3
+ export type HarnessErrorDetails = Readonly<Record<string, string | number | boolean>>;
4
+ export interface HarnessErrorOptions {
5
+ readonly cause?: unknown;
6
+ readonly details?: HarnessErrorDetails;
7
+ }
8
+ /**
9
+ * The sole error shape produced by Harness itself. Message text is explanatory;
10
+ * callers must branch on `code`.
11
+ */
12
+ export declare class HarnessError extends Error {
13
+ readonly code: HarnessErrorCode;
14
+ readonly details: HarnessErrorDetails;
15
+ constructor(code: HarnessErrorCode, message: string, options?: HarnessErrorOptions);
16
+ }
17
+ export declare function isHarnessError(error: unknown): error is HarnessError;
package/dist/errors.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The sole error shape produced by Harness itself. Message text is explanatory;
3
+ * callers must branch on `code`.
4
+ */
5
+ export class HarnessError extends Error {
6
+ code;
7
+ details;
8
+ constructor(code, message, options = {}) {
9
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
10
+ this.name = "HarnessError";
11
+ this.code = code;
12
+ this.details = Object.freeze({ ...(options.details ?? {}) });
13
+ }
14
+ }
15
+ export function isHarnessError(error) {
16
+ return error instanceof HarnessError;
17
+ }
@@ -0,0 +1,11 @@
1
+ export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError } from "./build/builder.js";
2
+ export { HarnessError, isHarnessError } from "./errors.js";
3
+ export type { HarnessErrorCode, HarnessErrorDetails, HarnessErrorOptions } from "./errors.js";
4
+ export { BuiltAgent } from "./build/agent.js";
5
+ export { adapter, middleware, model, tool } from "./build/helpers.js";
6
+ export type { AgentManifest } from "./types/manifest.js";
7
+ export type { ModelCandidate, ModelControls, ModelDirective, ModelEvidence, ModelFinishReason, ModelAdapter, ModelAdapterContext, ContextContributor, ContextMutationOptions, ContextSnapshot, ModelCall, ModelCallTool, ModelOutputBlock, PromptContentPart, PromptItem, ModelConfigurationContributor, ModelConfigurationInstruction, ModelConfigurationMutationOptions, ModelConfigurationSnapshot, ModelConfigurationTool, ModelRequest, ModelToolCall, ModelUsage, } from "./types/model.js";
8
+ export type { BoundMiddleware, StepInput, StepMiddleware, StepRequest, StepResponse, } from "./types/middleware.js";
9
+ export type { BuildDiagnostic, ContextItem, JsonObject, JsonPrimitive, JsonValue, ObserveEvent, ObserveModelConfigurationSnapshot, ObserveModelRequested, ObserveSealedCall, ObserveToolSnapshot, Observer, Tripwire, } from "./types/shared.js";
10
+ export type { InteractionReply, InputCompletion, InputEvent, InputHandle, InputOptions, MessageInput, Session, SessionInput, SessionEvent, SessionOptions, SessionSnapshot, TranscriptEntry, } from "./types/session.js";
11
+ export type { BoundToolSchema, BoundToolDefinition, Interaction, PreflightOutcome, RequiredInteraction, SealedToolCall, ToolAdapter, AdapterExecutionOptions, ToolContent, ToolDefinition, ToolExecutionResume, ToolObjectSchema, ToolOutcome, ToolResult, } from "./types/tool.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError } from "./build/builder.js";
2
+ export { HarnessError, isHarnessError } from "./errors.js";
3
+ export { BuiltAgent } from "./build/agent.js";
4
+ export { adapter, middleware, model, tool } from "./build/helpers.js";
@@ -0,0 +1,6 @@
1
+ import { HarnessError } from "./errors.js";
2
+ import type { ModelCandidate, ModelDirective, ModelOutputBlock } from "./types/model.js";
3
+ export declare function normalizeDirective(value: unknown): ModelDirective | HarnessError;
4
+ export declare function sameDirective(left: ModelDirective, right: ModelDirective): boolean;
5
+ export declare function textFromOutput(output: readonly ModelOutputBlock[]): string;
6
+ export declare function normalizeCandidate(value: ModelCandidate | string): ModelCandidate;
@@ -0,0 +1,211 @@
1
+ import { HarnessError, isHarnessError } from "./errors.js";
2
+ import { digest } from "./utils/digest.js";
3
+ import { copyJsonObject } from "./utils/immutable.js";
4
+ const FINISH_REASONS = new Set([
5
+ "stop",
6
+ "length",
7
+ "tool-calls",
8
+ "content-filter",
9
+ "other",
10
+ ]);
11
+ const USAGE_TOKEN_KEYS = [
12
+ "inputTokens",
13
+ "outputTokens",
14
+ "totalTokens",
15
+ "cachedTokens",
16
+ "reasoningTokens",
17
+ ];
18
+ export function normalizeDirective(value) {
19
+ if (!value || typeof value !== "object" || Array.isArray(value))
20
+ return new HarnessError("model.invalid-directive", "Model directive must be an object");
21
+ const keys = Object.keys(value);
22
+ for (const key of keys) {
23
+ if (key !== "id" && key !== "controls" && key !== "config")
24
+ return new HarnessError("model.invalid-directive", `Model directive has unknown key '${key}'`, {
25
+ details: { path: key },
26
+ });
27
+ }
28
+ const raw = value;
29
+ if (raw.id !== undefined && (typeof raw.id !== "string" || raw.id.length === 0))
30
+ return new HarnessError("model.invalid-directive", "Model directive id must be a non-empty string", {
31
+ details: { path: "id" },
32
+ });
33
+ let controls;
34
+ if (raw.controls !== undefined) {
35
+ const normalized = normalizeControls(raw.controls);
36
+ if (isHarnessError(normalized))
37
+ return normalized;
38
+ controls = normalized;
39
+ }
40
+ let config;
41
+ if (raw.config !== undefined) {
42
+ try {
43
+ config = copyJsonObject(raw.config, "model directive config");
44
+ }
45
+ catch (error) {
46
+ return isHarnessError(error)
47
+ ? error
48
+ : new HarnessError("model.invalid-directive", String(error), { cause: error });
49
+ }
50
+ }
51
+ return Object.freeze({
52
+ ...(typeof raw.id === "string" ? { id: raw.id } : {}),
53
+ ...(controls === undefined ? {} : { controls }),
54
+ ...(config === undefined ? {} : { config }),
55
+ });
56
+ }
57
+ export function sameDirective(left, right) {
58
+ return digest(left) === digest(right);
59
+ }
60
+ export function textFromOutput(output) {
61
+ return output.flatMap((block) => (block.type === "text" ? [block.text] : [])).join("");
62
+ }
63
+ export function normalizeCandidate(value) {
64
+ if (typeof value === "string")
65
+ return Object.freeze({ output: freezeBlocks([{ type: "text", text: value }]) });
66
+ if (!value || typeof value !== "object" || Array.isArray(value))
67
+ throw invalidCandidate("Model candidate must be an object or string");
68
+ if (!Array.isArray(value.output))
69
+ throw invalidCandidate("Model candidate output must be an array", "output");
70
+ const output = freezeBlocks(value.output.map((block, index) => normalizeBlock(block, index)));
71
+ return Object.freeze({
72
+ output,
73
+ ...(value.finishReason === undefined
74
+ ? {}
75
+ : { finishReason: normalizeFinishReason(value.finishReason) }),
76
+ ...(value.usage === undefined ? {} : { usage: normalizeUsage(value.usage) }),
77
+ ...(value.evidence === undefined ? {} : { evidence: normalizeEvidence(value.evidence) }),
78
+ });
79
+ }
80
+ function normalizeControls(value) {
81
+ if (!value || typeof value !== "object" || Array.isArray(value))
82
+ return new HarnessError("model.invalid-directive", "Model directive controls must be an object", {
83
+ details: { path: "controls" },
84
+ });
85
+ const keys = Object.keys(value);
86
+ for (const key of keys) {
87
+ if (key !== "temperature" && key !== "maxOutputTokens")
88
+ return new HarnessError("model.invalid-directive", `Model directive controls has unknown key '${key}'`, { details: { path: `controls.${key}` } });
89
+ }
90
+ const raw = value;
91
+ if (raw.temperature !== undefined && !isFiniteNumber(raw.temperature))
92
+ return new HarnessError("model.invalid-directive", "Model directive temperature must be a finite number", { details: { path: "controls.temperature" } });
93
+ if (raw.maxOutputTokens !== undefined && !isNonNegativeInteger(raw.maxOutputTokens))
94
+ return new HarnessError("model.invalid-directive", "Model directive maxOutputTokens must be a non-negative integer", { details: { path: "controls.maxOutputTokens" } });
95
+ return Object.freeze({
96
+ ...(raw.temperature === undefined ? {} : { temperature: raw.temperature }),
97
+ ...(raw.maxOutputTokens === undefined ? {} : { maxOutputTokens: raw.maxOutputTokens }),
98
+ });
99
+ }
100
+ function normalizeBlock(value, index) {
101
+ if (!value || typeof value !== "object" || Array.isArray(value))
102
+ throw invalidCandidate(`Model output[${index}] must be an object`, `output[${index}]`);
103
+ const block = value;
104
+ if (block.type === "text" || block.type === "reasoning") {
105
+ rejectUnknownKeys(value, ["type", "text"], `Model output[${index}]`);
106
+ const text = value.text;
107
+ if (typeof text !== "string")
108
+ throw invalidCandidate(`Model output[${index}].text must be a string`, `output[${index}].text`);
109
+ return Object.freeze({ type: block.type, text });
110
+ }
111
+ if (block.type === "tool-call") {
112
+ rejectUnknownKeys(value, ["type", "id", "name", "args", "raw"], `Model output[${index}]`);
113
+ const raw = value;
114
+ if (raw.id !== undefined && typeof raw.id !== "string")
115
+ throw invalidCandidate(`Model output[${index}].id must be a string`, `output[${index}].id`);
116
+ if (raw.name !== undefined && typeof raw.name !== "string")
117
+ throw invalidCandidate(`Model output[${index}].name must be a string`, `output[${index}].name`);
118
+ let args;
119
+ try {
120
+ args = copyJsonObject(raw.args, `Model output[${index}].args`);
121
+ }
122
+ catch (error) {
123
+ throw isHarnessError(error) && error.code === "model.invalid-candidate"
124
+ ? error
125
+ : invalidCandidate(`Model output[${index}].args must be a JSON object`, `output[${index}].args`, error);
126
+ }
127
+ if (raw.raw !== undefined && typeof raw.raw !== "string")
128
+ throw invalidCandidate(`Model output[${index}].raw must be a string`, `output[${index}].raw`);
129
+ return Object.freeze({
130
+ type: "tool-call",
131
+ id: typeof raw.id === "string" ? raw.id : "",
132
+ name: typeof raw.name === "string" ? raw.name : "",
133
+ args,
134
+ ...(raw.raw === undefined ? {} : { raw: raw.raw }),
135
+ });
136
+ }
137
+ throw invalidCandidate(`Model output[${index}] has unknown type`, `output[${index}].type`);
138
+ }
139
+ function normalizeFinishReason(value) {
140
+ if (value === "error" || value === "aborted")
141
+ throw invalidCandidate("Model candidate finishReason cannot be error or aborted", "finishReason");
142
+ if (typeof value !== "string" || !FINISH_REASONS.has(value))
143
+ throw invalidCandidate("Model candidate finishReason is invalid", "finishReason");
144
+ return value;
145
+ }
146
+ function normalizeUsage(value) {
147
+ if (!value || typeof value !== "object" || Array.isArray(value))
148
+ throw invalidCandidate("Model candidate usage must be an object", "usage");
149
+ rejectUnknownKeys(value, [...USAGE_TOKEN_KEYS, "costUsd"], "Model candidate usage");
150
+ const raw = value;
151
+ const usage = {};
152
+ for (const key of USAGE_TOKEN_KEYS) {
153
+ if (raw[key] === undefined)
154
+ continue;
155
+ if (!isNonNegativeInteger(raw[key]))
156
+ throw invalidCandidate(`Model candidate usage.${key} must be a non-negative integer`, `usage.${key}`);
157
+ usage[key] = raw[key];
158
+ }
159
+ if (raw.costUsd !== undefined) {
160
+ if (!isFiniteNumber(raw.costUsd))
161
+ throw invalidCandidate("Model candidate usage.costUsd must be a finite number", "usage.costUsd");
162
+ usage.costUsd = raw.costUsd;
163
+ }
164
+ return Object.freeze(usage);
165
+ }
166
+ function normalizeEvidence(value) {
167
+ if (!value || typeof value !== "object" || Array.isArray(value))
168
+ throw invalidCandidate("Model candidate evidence must be an object", "evidence");
169
+ rejectUnknownKeys(value, ["requestId", "resolvedModel", "warnings", "extras"], "Model candidate evidence");
170
+ const raw = value;
171
+ if (raw.requestId !== undefined && typeof raw.requestId !== "string")
172
+ throw invalidCandidate("Model candidate evidence.requestId must be a string", "evidence.requestId");
173
+ if (raw.resolvedModel !== undefined && typeof raw.resolvedModel !== "string")
174
+ throw invalidCandidate("Model candidate evidence.resolvedModel must be a string", "evidence.resolvedModel");
175
+ let warnings;
176
+ if (raw.warnings !== undefined) {
177
+ if (!Array.isArray(raw.warnings) || raw.warnings.some((item) => typeof item !== "string"))
178
+ throw invalidCandidate("Model candidate evidence.warnings must be an array of strings", "evidence.warnings");
179
+ warnings = Object.freeze([...raw.warnings]);
180
+ }
181
+ const extras = raw.extras === undefined
182
+ ? undefined
183
+ : copyJsonObject(raw.extras, "Model candidate evidence.extras");
184
+ return Object.freeze({
185
+ ...(typeof raw.requestId === "string" ? { requestId: raw.requestId } : {}),
186
+ ...(typeof raw.resolvedModel === "string" ? { resolvedModel: raw.resolvedModel } : {}),
187
+ ...(warnings === undefined ? {} : { warnings }),
188
+ ...(extras === undefined ? {} : { extras }),
189
+ });
190
+ }
191
+ function freezeBlocks(blocks) {
192
+ return Object.freeze(blocks.map((block) => Object.freeze(block)));
193
+ }
194
+ function rejectUnknownKeys(value, allowed, path) {
195
+ for (const key of Object.keys(value)) {
196
+ if (!allowed.includes(key))
197
+ throw invalidCandidate(`${path} has unknown key '${key}'`, `${path}.${key}`);
198
+ }
199
+ }
200
+ function invalidCandidate(message, path, cause) {
201
+ return new HarnessError("model.invalid-candidate", message, {
202
+ ...(cause === undefined ? {} : { cause }),
203
+ ...(path === undefined ? {} : { details: { path } }),
204
+ });
205
+ }
206
+ function isFiniteNumber(value) {
207
+ return typeof value === "number" && Number.isFinite(value);
208
+ }
209
+ function isNonNegativeInteger(value) {
210
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
211
+ }
@@ -0,0 +1,11 @@
1
+ import type { SessionEvent } from "../types/session.js";
2
+ /** Session-lifetime conversation log. Each stream() call replays from the start. */
3
+ export declare class SessionEventLog implements AsyncIterable<SessionEvent> {
4
+ private readonly events;
5
+ private readonly subscribers;
6
+ private done;
7
+ emit(event: SessionEvent): void;
8
+ finish(): void;
9
+ [Symbol.asyncIterator](): AsyncIterator<SessionEvent>;
10
+ private wake;
11
+ }
@@ -0,0 +1,63 @@
1
+ /** Session-lifetime conversation log. Each stream() call replays from the start. */
2
+ export class SessionEventLog {
3
+ events = [];
4
+ subscribers = [];
5
+ done = false;
6
+ emit(event) {
7
+ if (this.done)
8
+ return;
9
+ this.events.push(event);
10
+ this.wake();
11
+ }
12
+ finish() {
13
+ if (this.done)
14
+ return;
15
+ this.done = true;
16
+ this.wake();
17
+ this.subscribers.length = 0;
18
+ }
19
+ [Symbol.asyncIterator]() {
20
+ let index = 0;
21
+ let closed = false;
22
+ const waiting = [];
23
+ this.subscribers.push(waiting);
24
+ const detach = () => {
25
+ const position = this.subscribers.indexOf(waiting);
26
+ if (position !== -1)
27
+ this.subscribers.splice(position, 1);
28
+ };
29
+ const complete = () => {
30
+ closed = true;
31
+ detach();
32
+ return { done: true, value: undefined };
33
+ };
34
+ const pull = () => {
35
+ if (closed)
36
+ return Promise.resolve({ done: true, value: undefined });
37
+ if (index < this.events.length)
38
+ return Promise.resolve({ done: false, value: this.events[index++] });
39
+ if (this.done)
40
+ return Promise.resolve(complete());
41
+ return new Promise((resolve) => {
42
+ waiting.push(() => {
43
+ void pull().then(resolve);
44
+ });
45
+ });
46
+ };
47
+ return {
48
+ next: pull,
49
+ return: () => {
50
+ const result = complete();
51
+ while (waiting.length)
52
+ waiting.shift()();
53
+ return Promise.resolve(result);
54
+ },
55
+ };
56
+ }
57
+ wake() {
58
+ for (const waiting of this.subscribers) {
59
+ while (waiting.length)
60
+ waiting.shift()();
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,29 @@
1
+ import type { InputEvent, InputOptions } from "../types/session.js";
2
+ import { SubmissionStream } from "./submission-stream.js";
3
+ export interface QueuedInput {
4
+ readonly event: InputEvent;
5
+ readonly options?: InputOptions;
6
+ readonly stream: SubmissionStream;
7
+ cancelled: boolean;
8
+ }
9
+ export interface QueueAbortHandlers {
10
+ readonly isActive: () => boolean;
11
+ readonly abortActive: (reason: unknown) => void;
12
+ readonly cancelQueued: () => void;
13
+ }
14
+ export declare function isInteractionReply(event: InputEvent): event is Extract<InputEvent, {
15
+ kind: "approve" | "respond";
16
+ }>;
17
+ export declare function snapshotInput(event: InputEvent): InputEvent;
18
+ /** Serializes ordinary input while allowing a matching interaction reply to resume immediately. */
19
+ export declare class InputQueue {
20
+ private readonly values;
21
+ get size(): number;
22
+ add(input: QueuedInput, priority?: boolean): void;
23
+ take(waitingForInteraction: boolean): QueuedInput | undefined;
24
+ takeInterrupts(): QueuedInput[];
25
+ remove(input: QueuedInput): boolean;
26
+ drain(): readonly QueuedInput[];
27
+ }
28
+ /** Binds one caller-provided signal to its queue entry and always removes the listener at completion. */
29
+ export declare function watchInputAbort(input: QueuedInput, handlers: QueueAbortHandlers): void;