@nylorun/harness 0.15.0-beta → 0.16.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.16.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 3a88f51: Ship Agent-Plugins (`plugin()` / `loadPlugin`), Skills (`load_skill` / skill resources), Runtime MCP pool + vault credentials, and manifest v3 capability fields. Validate completed tool `output` against the tool output schema so ordinary tools with `outputSchema` no longer false-fail as `tool.invalid-output`.
8
+
9
+ ### Patch Changes
10
+
11
+ - Pin core to the tested release.
12
+ - Updated dependencies [3a88f51]
13
+ - @nylorun/core@0.2.0-beta
14
+
3
15
  ## 0.15.0-beta
4
16
 
5
17
  ### Major Changes
package/HOST_CONTRACT.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # Durable execution and session protocol — beta freeze
2
2
 
3
- Artifact: `@nylorun/harness@0.14.0-beta.3`. Package version is separate from protocol 1, definition schema 2, durable checkpoint 1 / engine `hosted-1`, and inner execution checkpoint 1. Public execution API and shared contracts are the compatibility boundary. Cloud installs published npm packages and does not import the OSS Runtime.
3
+ Artifact: `@nylorun/harness@0.14.0-beta.3`. Package version is separate from protocol 1, definition schema 3, durable checkpoint 1 / engine `hosted-1`, and inner execution checkpoint 1. Public execution API and shared contracts are the compatibility boundary. Cloud installs published npm packages and does not import the OSS Runtime.
4
4
 
5
5
  ## HTTP (both hosts)
6
6
 
7
- Bearer application credentials: `PUT /v1/agents/:agentId` (`PutAgentRequest`: requestId, complete manifest, implementationVersion); `PUT /v1/sessions/:sessionId` (`PutSessionRequest`: requestId, agentId, trusted ownerUserId, optional info); `GET /v1/sessions/:sessionId`; `POST /v1/sessions/:sessionId/commands`; `GET /v1/sessions/:sessionId/items?cursor=...`; `GET /v1/sessions/:sessionId/events?cursor=...` (SSE, Last-Event-ID also accepted). Commands use `SessionCommandSchema`. Message input is `content`, not text. Runtime configuration selects models; saved definitions do not. Session creation pins manifest hash and implementation version. GET session returns id, agentId, ownerUserId, status, activeTurnId, plus inspectable waits/uncertainty as available. History returns `{items: LiveEvent[], cursor: string|null}`; empty history cursor is null.
7
+ Bearer application credentials: `PUT /v1/agents/:agentId` (`PutAgentRequest`: requestId, complete manifest, implementationVersion, optional pluginRoots); `PUT /v1/sessions/:sessionId` (`PutSessionRequest`: requestId, agentId, trusted ownerUserId, optional info, optional vaultIds, optional credentialSelections); `GET /v1/sessions/:sessionId`; `POST /v1/sessions/:sessionId/commands`; `GET /v1/sessions/:sessionId/items?cursor=...`; `GET /v1/sessions/:sessionId/events?cursor=...` (SSE, Last-Event-ID also accepted); vault administration `POST/GET/DELETE /v1/vaults` and `/v1/vaults/:vaultId/credentials`. Commands use `SessionCommandSchema`. Vault creates and credential rotations use `idempotencyKey`; requestId is excluded from comparison; the same key and body replay the stored metadata response; a changed body is 409. Credential responses omit secret values. `GET /v1/vaults?ownerUserId=` lists that user's vaults. A session attaches only vaults with the same ownerUserId. A later session PUT may replace vaultIds and credentialSelections; agentId, ownerUserId, and info stay the creation identity. GET session returns id, agentId, ownerUserId, status, activeTurnId, vaultIds, credentialSelections, mcpSnapshot, mcpDiagnostics, plus inspectable waits/uncertainty as available. mcpSnapshot is null until the first turn discovers MCP tools; it is then pinned for the session and contains no secrets. pluginRoots are stored beside the agent definition and are not part of the manifest hash. History returns `{items: LiveEvent[], cursor: string|null}`; empty history cursor is null. Executor credentials cannot call vault routes.
8
8
 
9
- Separate scoped executor bearer credentials: `GET /v1/executors/connect` emits `event: work_available` / `data: {"type":"work_available"}` after connection and committed work; `GET /v1/actions` returns `{actions: Action[]}`; `POST /v1/actions/:actionId/claim` uses ActionClaimRequest; `POST /v1/actions/:actionId/heartbeat` uses ActionHeartbeatRequest and returns leaseExpiresAt. Claim returns ActionClaim. Action result is the `action_result` session command with executor authorization, never observer authorization. Claim identity is server-generated and generation monotonically increases. Credentials limit agentId, manifestHash, and implementationVersion; clients cannot broaden their scope. Subscribe before discovery; rediscover after reconnect; notifications never authorize execution.
9
+ Separate scoped executor bearer credentials: `GET /v1/executors/connect` emits `event: work_available` / `data: {"type":"work_available"}` after connection and committed work; `GET /v1/actions` returns `{actions: Action[]}`; `POST /v1/actions/:actionId/claim` uses ActionClaimRequest; `POST /v1/actions/:actionId/heartbeat` uses ActionHeartbeatRequest and returns leaseExpiresAt. Claim returns ActionClaim. Action result is the `action_result` session command with executor authorization, never observer authorization. Claim identity is server-generated and generation monotonically increases. Credentials authorize an executor by agentId. A manifestHash stored on an existing token is ignored, and claim requests do not send one. In-flight actions for that agent stay claimable after the registered digest changes. Clients cannot broaden their agent scope. Subscribe before discovery; rediscover after reconnect; notifications never authorize execution.
10
10
 
11
11
  All command idempotency keys bind to canonical request content within a session; requestId is transport metadata and is excluded from content comparison; exact semantic duplicates return the stored original response; changed content conflicts (409). A completed action's matching claim/generation/outcome returns its original receipt even after expiry. Conflicting duplicates and any other stale/expired claim are rejected. Heartbeat cannot resurrect an expired claim. Expired in-flight customer work is uncertain, not automatically reoffered. Results after cancellation cannot resume a turn. Application observers cannot claim or submit action results. No hardcoded production credentials.
12
12
 
13
13
  ## Engine host
14
14
 
15
- `createDurableCheckpoint({manifest,sessionId,turnId,input,state?,info?,segment?})`, `runDurable({manifest,checkpoint,host,signal?})` from `/run`. `DurableHost.resolveEffect(HostEffect)` atomically creates or reads an individually identified effect and returns `{status:'completed',outcome:{value,statePatch?}}` or `{status:'pending'|'uncertain'}`. Model effect `input` is the complete ModelCall (instructions/history/tools/output schema); context contains a JSON projection of ModelRequest and invocationId. Its completed value is a ModelCandidate or string. Runtime provider code owns credentials and invocation. Tools/hooks have capabilityId, toolName for tools, serialized input and context. Tool result value is a ToolOutcome or a plain tool output; statePatch contains changed session keys. Before/after hook values are Patch/Decision. Tool context includes invocationId, resume, state, info and stable idempotencyKey. Customer implementationVersion is pinned by the runtime outside durable checkpoints.
15
+ `createDurableCheckpoint({manifest,sessionId,turnId,input,state?,info?,segment?})`, `runDurable({manifest,checkpoint,host,signal?,sessionTools?})` from `/run`. `DurableHost.resolveEffect(HostEffect)` atomically creates or reads an individually identified effect and returns `{status:'completed',outcome:{value,statePatch?}}` or `{status:'pending'|'uncertain'}`. Model effect `input` is the complete ModelCall (instructions/history/tools/output schema); context contains a JSON projection of ModelRequest and invocationId. Its completed value is a ModelCandidate or string. Runtime provider code owns credentials and invocation. Tools/hooks have capabilityId, toolName for tools, serialized input and context. Tool result value is a ToolOutcome or a plain tool output; statePatch contains changed session keys. Before/after hook values are Patch/Decision. Tool context includes invocationId, resume, state, info and stable idempotencyKey. Customer implementationVersion is pinned by the runtime outside durable checkpoints.
16
16
 
17
17
  Persist checkpoint BEFORE scheduling, effect intent BEFORE provider invocation or notification, and outcome BEFORE resumption. Journaling an entire runDurable call as one replayable operation is forbidden. The durable path deterministically reconstructs a segment from its immutable starting checkpoint and individually cached effect outcomes. A waiting result returns that starting checkpoint: do not replace it with an intermediate preview. The journal is required continuation data and must be retained with the checkpoint. Stable IDs include turn/segment and deterministic invocation identity. There is no live continuation or request-held promise while waiting. Runtimes serialize work per session, check cancellation before effects and before settlement, and fence ownership. Lost provider acknowledgement becomes uncertain; do not repeat it merely because a worker restarts.
18
18
 
@@ -22,6 +22,6 @@ Host resolveEffect must bind the effect ID to the original request and reject dr
22
22
 
23
23
  ## Definitions, validation, compatibility
24
24
 
25
- `/define` is execution-free and browser-bundleable; Agent has no `.run()`. Explicit local execution uses `/run` run + bindingFromAgent(agent.build()). `/contracts` never imports checkpoint internals. Tools and before/after hooks are supported remotely. Arbitrary legacy middleware closures are rejected for durable definitions because they cannot be reconstructed from manifests. JSON schemas are reconstructed through Zod's JSON Schema importer; unsupported schemas fail closed. The original manifest hash must survive reconstruction. Model configuration is host-owned. The executor validates its local manifest hash and implementationVersion before claiming and validates tool inputs/outputs locally.
25
+ `/define` is execution-free and browser-bundleable; Agent has no `.run()`. Explicit local execution uses `/run` run + bindingFromAgent(agent.build()). `/contracts` never imports checkpoint internals. Tools and before/after hooks are supported remotely. Arbitrary legacy middleware closures are rejected for durable definitions because they cannot be reconstructed from manifests. JSON schemas are reconstructed through Zod's JSON Schema importer; unsupported schemas fail closed. The original manifest document must survive reconstruction. Runtime is the only hasher: it hashes the accepted manifest and pins that digest on the session and on each action. Agents do not hash. Model configuration is host-owned. The executor claims by agent id. Runtime checks a successful tool result against the output schema copied onto the action and stores a schema mismatch as a failed outcome on a completed action.
26
26
 
27
27
  No session migration, arbitrary checkpoint migration, broad model-support, or conformance claims follow from compiling this interface. Studio and conformance tooling remain outside production harness.
@@ -8,7 +8,7 @@ export class ToolRegistry {
8
8
  owners = new WeakMap();
9
9
  constructor(middleware, snapshots) {
10
10
  for (const capability of middleware) {
11
- for (const tool of capability.tools ?? []) {
11
+ for (const tool of [...(capability.tools ?? []), ...(capability.sessionTools ?? [])]) {
12
12
  const key = JSON.stringify([capability.id, tool.name]);
13
13
  if (this.tools.has(key) || this.owners.has(tool))
14
14
  throw new HarnessError("tool.invalid", `Duplicate registered tool '${tool.name}' in '${capability.id}'`);
@@ -42,13 +42,15 @@ export async function applyBeforeModelCall(ctx, configuration) {
42
42
  }
43
43
  if (patch.tools) {
44
44
  for (const [name, on] of Object.entries(patch.tools)) {
45
- const known = ctx.agent.manifest.capabilities.some((cap) => cap.tools?.some((tool) => tool.name === name));
45
+ const known = ctx.agent.manifest.capabilities.some((cap) => cap.tools?.some((tool) => tool.name === name) ||
46
+ ctx.agent.implementations[cap.id]?.tools?.[name] !== undefined);
46
47
  if (!known)
47
48
  throw new HarnessError("configuration.invalid", `beforeModelCall referenced unknown tool '${name}'`);
48
49
  if (!agentLevel) {
49
- const owned = ctx.agent.manifest.capabilities
50
+ const declared = ctx.agent.manifest.capabilities
50
51
  .find((cap) => cap.id === id)
51
52
  ?.tools?.some((tool) => tool.name === name);
53
+ const owned = declared || ctx.agent.implementations[id]?.tools?.[name] !== undefined;
52
54
  if (!owned)
53
55
  throw new HarnessError("configuration.invalid", `Capability '${id}' beforeModelCall may only toggle its own tools`);
54
56
  }
@@ -50,6 +50,14 @@ export type DurableResult = {
50
50
  readonly checkpoint: DurableCheckpoint;
51
51
  readonly result: RunResult<unknown>;
52
52
  };
53
+ /** Discovered tool advertised for this execution. It is not part of the hashed manifest. */
54
+ export interface DurableSessionTool {
55
+ readonly capabilityId: string;
56
+ readonly name: string;
57
+ readonly description?: string;
58
+ readonly inputSchema: JsonObject;
59
+ readonly outputSchema?: JsonObject;
60
+ }
53
61
  export declare function createDurableCheckpoint(input: {
54
62
  manifest: AgentManifest;
55
63
  sessionId: string;
@@ -68,4 +76,5 @@ export declare function runDurable(options: {
68
76
  checkpoint: DurableCheckpoint;
69
77
  host: DurableHost;
70
78
  signal?: AbortSignal;
79
+ sessionTools?: readonly DurableSessionTool[];
71
80
  }): Promise<DurableResult>;
@@ -72,54 +72,59 @@ export async function runDurable(options) {
72
72
  return result.outcome;
73
73
  };
74
74
  let patchTail = Promise.resolve();
75
+ const hostedTool = (capabilityId, tool) => ({
76
+ name: tool.name,
77
+ ...(tool.description === undefined ? {} : { description: tool.description }),
78
+ inputSchema: schemaFromJSON(tool.inputSchema),
79
+ ...(tool.outputSchema ? { outputSchema: schemaFromJSON(tool.outputSchema) } : {}),
80
+ async execute(args, ctx) {
81
+ const previous = patchTail;
82
+ let release;
83
+ patchTail = new Promise((resolve) => {
84
+ release = resolve;
85
+ });
86
+ try {
87
+ const outcome = await effect("tool", args, {
88
+ executionId: ctx.executionId,
89
+ turnId: ctx.turnId,
90
+ stepId: ctx.stepId,
91
+ callId: ctx.callId,
92
+ invocationId: ctx.invocationId,
93
+ idempotencyKey: ctx.idempotencyKey,
94
+ info: ctx.info,
95
+ state: ctx.state.entries(),
96
+ resume: ctx.resume,
97
+ }, capabilityId, tool.name, ctx.invocationId);
98
+ // Apply concurrently resolved patches in manifest call order, independent of host I/O timing.
99
+ await previous;
100
+ for (const [key, value] of Object.entries(outcome.statePatch ?? {}))
101
+ ctx.state.set(key, value);
102
+ const value = outcome.value;
103
+ return value?.kind === "interaction-required"
104
+ ? {
105
+ ...value,
106
+ interaction: {
107
+ ...value.interaction,
108
+ id: `${checkpoint.turnId}:${checkpoint.segment}:interaction:${ctx.invocationId}`,
109
+ },
110
+ }
111
+ : value;
112
+ }
113
+ finally {
114
+ await previous;
115
+ release();
116
+ }
117
+ },
118
+ });
75
119
  const implementations = {};
120
+ const sessionTools = options.sessionTools ?? [];
76
121
  for (const capability of manifest.capabilities) {
77
122
  const tools = {};
78
123
  for (const tool of capability.tools ?? [])
79
- tools[tool.name] = {
80
- name: tool.name,
81
- ...(tool.description === undefined ? {} : { description: tool.description }),
82
- inputSchema: schemaFromJSON(tool.inputSchema),
83
- ...(tool.outputSchema ? { outputSchema: schemaFromJSON(tool.outputSchema) } : {}),
84
- async execute(args, ctx) {
85
- const previous = patchTail;
86
- let release;
87
- patchTail = new Promise((resolve) => {
88
- release = resolve;
89
- });
90
- try {
91
- const outcome = await effect("tool", args, {
92
- executionId: ctx.executionId,
93
- turnId: ctx.turnId,
94
- stepId: ctx.stepId,
95
- callId: ctx.callId,
96
- invocationId: ctx.invocationId,
97
- idempotencyKey: ctx.idempotencyKey,
98
- info: ctx.info,
99
- state: ctx.state.entries(),
100
- resume: ctx.resume,
101
- }, capability.id, tool.name, ctx.invocationId);
102
- // Apply concurrently resolved patches in manifest call order, independent of host I/O timing.
103
- await previous;
104
- for (const [key, value] of Object.entries(outcome.statePatch ?? {}))
105
- ctx.state.set(key, value);
106
- const value = outcome.value;
107
- return value?.kind === "interaction-required"
108
- ? {
109
- ...value,
110
- interaction: {
111
- ...value.interaction,
112
- id: `${checkpoint.turnId}:${checkpoint.segment}:interaction:${ctx.invocationId}`,
113
- },
114
- }
115
- : value;
116
- }
117
- finally {
118
- await previous;
119
- release();
120
- }
121
- },
122
- };
124
+ tools[tool.name] = hostedTool(capability.id, tool);
125
+ for (const tool of sessionTools)
126
+ if (tool.capabilityId === capability.id)
127
+ tools[tool.name] = hostedTool(capability.id, tool);
123
128
  implementations[capability.id] = {
124
129
  tools,
125
130
  ...(capability.beforeModelCall
@@ -135,7 +140,14 @@ export async function runDurable(options) {
135
140
  : {}),
136
141
  };
137
142
  }
138
- const definition = definitionFor(agentFrom(manifest, implementations));
143
+ const definition = definitionFor(agentFrom(manifest, implementations, sessionTools.length === 0
144
+ ? undefined
145
+ : {
146
+ sessionTools: sessionTools.map((tool) => ({
147
+ capabilityId: tool.capabilityId,
148
+ name: tool.name,
149
+ })),
150
+ }));
139
151
  // Manifest reconstruction must retain definition identity, including schemas and instructions.
140
152
  if (definition.hash !== checkpoint.manifestHash)
141
153
  throw new HarnessError("execution.incompatible", "Reconstructed definition hash mismatch");
@@ -29,5 +29,5 @@ export declare function bindingFromAgent(agent: BuiltAgent): RunBinding;
29
29
  export { checkCompatibility, createExecutionState, validateExecutionState, execute };
30
30
  export type { Implementations, ExecutionState, RunResult, RunOptions };
31
31
  export { createDurableCheckpoint, runDurable } from "./durable.js";
32
- export type { DurableCheckpoint, HostEffect, EffectResolution, DurableHost, DurableResult, } from "./durable.js";
32
+ export type { DurableCheckpoint, DurableSessionTool, HostEffect, EffectResolution, DurableHost, DurableResult, } from "./durable.js";
33
33
  export type { ModelAdapter, ModelCandidate, ModelCall, ModelRequest } from "@nylorun/core/define";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/harness",
3
- "version": "0.15.0-beta",
3
+ "version": "0.16.0-beta",
4
4
  "description": "Nylorun's TypeScript agent execution engine. See github.com/nylorun/harness.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -70,15 +70,15 @@
70
70
  "devDependencies": {
71
71
  "@types/node": "^26.5.0",
72
72
  "@typescript/native": "npm:typescript@^7.0.2",
73
- "prettier": "^3.9.6",
73
+ "prettier": "^3.9.8",
74
74
  "typescript": "npm:@typescript/typescript6@^6.0.2",
75
- "vitest": "^5.0.0",
76
- "zod": "^4.1.12"
75
+ "vitest": "^5.0.1",
76
+ "zod": "^4.6.5"
77
77
  },
78
78
  "dependencies": {
79
- "@nylorun/core": "0.1.1-beta"
79
+ "@nylorun/core": "0.2.0-beta"
80
80
  },
81
81
  "peerDependencies": {
82
- "zod": "^4.5.4"
82
+ "zod": "^4.6.5"
83
83
  }
84
84
  }