stable-harness 0.0.106 → 0.0.107
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/docs/guides/integration-guide.md +30 -2
- package/docs/protocols/agent-protocols.md +33 -3
- package/node_modules/@stable-harness/adapter-deepagents/package.json +2 -2
- package/node_modules/@stable-harness/adapter-langgraph/package.json +2 -2
- package/node_modules/@stable-harness/core/package.json +3 -3
- package/node_modules/@stable-harness/governance/package.json +1 -1
- package/node_modules/@stable-harness/memory/package.json +1 -1
- package/node_modules/@stable-harness/protocols/dist/src/agent-protocols.d.ts +19 -0
- package/node_modules/@stable-harness/protocols/dist/src/agent-protocols.js +1 -1
- package/node_modules/@stable-harness/protocols/dist/src/index.d.ts +1 -1
- package/node_modules/@stable-harness/protocols/dist/src/index.js +1 -1
- package/node_modules/@stable-harness/protocols/package.json +2 -2
- package/node_modules/@stable-harness/tool-gateway/package.json +1 -1
- package/node_modules/@stable-harness/workspace-yaml/package.json +2 -2
- package/package.json +9 -9
- package/packages/adapter-deepagents/package.json +2 -2
- package/packages/adapter-langgraph/package.json +2 -2
- package/packages/cli/dist/src/args.d.ts +1 -1
- package/packages/cli/dist/src/args.js +1 -1
- package/packages/cli/dist/src/cli.js +1 -1
- package/packages/cli/dist/src/console/session.js +1 -1
- package/packages/cli/package.json +8 -8
- package/packages/core/package.json +3 -3
- package/packages/evaluation/package.json +2 -2
- package/packages/governance/package.json +1 -1
- package/packages/memory/package.json +1 -1
- package/packages/protocols/dist/src/agent-protocols.d.ts +19 -0
- package/packages/protocols/dist/src/agent-protocols.js +1 -1
- package/packages/protocols/dist/src/index.d.ts +1 -1
- package/packages/protocols/dist/src/index.js +1 -1
- package/packages/protocols/package.json +2 -2
- package/packages/tool-gateway/package.json +1 -1
- package/packages/workspace-yaml/package.json +2 -2
|
@@ -90,6 +90,12 @@ supports ordinary prompts plus slash commands:
|
|
|
90
90
|
| `/debug` | Print runtime inspection JSON. |
|
|
91
91
|
| `/exit` | Leave console mode. |
|
|
92
92
|
|
|
93
|
+
When the console uses `--protocol a2a`, `--protocol agui`, or `--protocol acp`,
|
|
94
|
+
the selected protocol carries the current session ID for prompt turns, but the
|
|
95
|
+
full operator commands still belong to `stable-runtime`. Use
|
|
96
|
+
`--protocol stable-runtime` when the console must list sessions, inspect
|
|
97
|
+
requests, manage memory, clear sessions, or stream native control-plane events.
|
|
98
|
+
|
|
93
99
|
## Embedded Runtime
|
|
94
100
|
|
|
95
101
|
Use the SDK when Stable Harness runs inside your service:
|
|
@@ -188,7 +194,22 @@ See [Stable Runtime HTTP + SSE](../protocols/http-runtime.md).
|
|
|
188
194
|
| LangGraph Studio | LangGraph-compatible server | Use for LangGraph/LangSmith Studio workflows. |
|
|
189
195
|
| A2A clients | A2A facade | Use when the caller already speaks A2A task/message APIs. |
|
|
190
196
|
| AG-UI clients | AG-UI facade | Use when the UI expects AG-UI event streams. |
|
|
191
|
-
| ACP clients | ACP HTTP JSON-RPC custom transport | Use for ACP-shaped clients
|
|
197
|
+
| ACP clients | ACP stdio or ACP HTTP JSON-RPC custom transport | Use stdio for ACP-shaped local process clients and HTTP JSON-RPC for server-style clients. |
|
|
198
|
+
|
|
199
|
+
Session support is intentionally layered:
|
|
200
|
+
|
|
201
|
+
| Surface | Session level |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| Stable Runtime HTTP + SSE | Full operator session API: request history, events, deletion, memory, approvals, and inspection. |
|
|
204
|
+
| SDK / in-process runtime | Full operator session API through `StableHarnessRuntime`. |
|
|
205
|
+
| OpenAI-compatible `/v1` | Chat continuity through session headers or metadata. |
|
|
206
|
+
| LangGraph-compatible | Maps LangGraph `thread_id` to Stable Runtime `sessionId`. |
|
|
207
|
+
| A2A | Maps A2A `contextId` or `taskId` to Stable Runtime `sessionId`. |
|
|
208
|
+
| AG-UI | Maps AG-UI `threadId` or `sessionId` to Stable Runtime `sessionId`. |
|
|
209
|
+
| ACP HTTP JSON-RPC | Uses ACP `session/*` methods and maps `sessionId` to Stable Runtime. |
|
|
210
|
+
|
|
211
|
+
Protocol facades preserve continuity for their ecosystems, but they do not
|
|
212
|
+
replace the Stable Runtime operator API.
|
|
192
213
|
|
|
193
214
|
## ACP, A2A, and AG-UI Facades
|
|
194
215
|
|
|
@@ -208,7 +229,14 @@ spec:
|
|
|
208
229
|
```
|
|
209
230
|
|
|
210
231
|
The server exposes A2A over HTTP+JSON/SSE, AG-UI over HTTP+SSE, and ACP over a
|
|
211
|
-
documented HTTP JSON-RPC custom transport.
|
|
232
|
+
documented HTTP JSON-RPC custom transport. Local ACP clients can also launch:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
stable-harness acp-stdio -w ./workspace
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The stdio entrypoint accepts one JSON-RPC message per line and reuses the same
|
|
239
|
+
ACP method handler as the HTTP endpoint. See
|
|
212
240
|
[ACP, A2A, and AG-UI Protocol Facades](../protocols/agent-protocols.md).
|
|
213
241
|
|
|
214
242
|
## Backend Adapters
|
|
@@ -23,6 +23,21 @@ You can also enable a single protocol by configuring one of `acp`, `a2a`, or
|
|
|
23
23
|
`agui` with `enabled: true`. The CLI starts one combined HTTP server for the
|
|
24
24
|
enabled adapter set.
|
|
25
25
|
|
|
26
|
+
Shared discovery endpoints:
|
|
27
|
+
|
|
28
|
+
- `GET /health`
|
|
29
|
+
- `GET /capabilities`
|
|
30
|
+
|
|
31
|
+
`/capabilities` returns a normalized manifest with enabled protocol IDs,
|
|
32
|
+
transports, session mapping level, and whether the protocol exposes the full
|
|
33
|
+
operator session API. ACP/A2A/AG-UI report `operatorApi: false`; Stable Runtime
|
|
34
|
+
HTTP/SSE remains the first-party operator API.
|
|
35
|
+
|
|
36
|
+
These facades preserve protocol-native continuity, but they are not the full
|
|
37
|
+
Stable Runtime operator API. Stable Runtime HTTP/SSE and the SDK remain the
|
|
38
|
+
first-party surfaces for listing sessions, deleting session history, inspecting
|
|
39
|
+
requests, managing memory, approvals, artifacts, and native event filters.
|
|
40
|
+
|
|
26
41
|
## A2A
|
|
27
42
|
|
|
28
43
|
A2A is exposed as an HTTP+JSON and SSE facade:
|
|
@@ -45,6 +60,9 @@ need explicit runtime fields such as `toolCall`, `workflow`, `agentId`, or
|
|
|
45
60
|
`sessionId`. External A2A clients can ignore that extension and use normal A2A
|
|
46
61
|
message parts.
|
|
47
62
|
|
|
63
|
+
Session mapping: `taskId` or message `contextId` becomes the Stable Runtime
|
|
64
|
+
`sessionId`.
|
|
65
|
+
|
|
48
66
|
## AG-UI
|
|
49
67
|
|
|
50
68
|
AG-UI is exposed as an HTTP+SSE event stream:
|
|
@@ -59,9 +77,19 @@ or `RunError`.
|
|
|
59
77
|
Stable Harness clients may include `runtimeRequest` in the run body to preserve
|
|
60
78
|
explicit runtime fields while still using the AG-UI event stream.
|
|
61
79
|
|
|
80
|
+
Session mapping: `threadId` or `sessionId` becomes the Stable Runtime
|
|
81
|
+
`sessionId`.
|
|
82
|
+
|
|
62
83
|
## ACP
|
|
63
84
|
|
|
64
|
-
ACP's standard transport is JSON-RPC over stdio. Stable Harness
|
|
85
|
+
ACP's standard transport is JSON-RPC over stdio. Stable Harness exposes a local
|
|
86
|
+
stdio entrypoint:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
stable-harness acp-stdio -w ./workspace
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The stdio entrypoint accepts one JSON-RPC message per line. Stable Harness also
|
|
65
93
|
exposes ACP over an HTTP JSON-RPC endpoint as a documented custom transport:
|
|
66
94
|
|
|
67
95
|
- `GET /acp/capabilities`
|
|
@@ -77,8 +105,10 @@ Supported methods:
|
|
|
77
105
|
- `session/cancel`
|
|
78
106
|
|
|
79
107
|
`session/prompt` maps to a native `RuntimeRequest` and returns the ACP stop
|
|
80
|
-
reason plus Stable Harness request metadata.
|
|
81
|
-
|
|
108
|
+
reason plus Stable Harness request metadata. Stdio and HTTP use the same
|
|
109
|
+
JSON-RPC method handler and do not change runtime semantics.
|
|
82
110
|
|
|
83
111
|
Stable Harness clients may include `params.runtimeRequest` to carry explicit
|
|
84
112
|
runtime fields through the ACP-shaped transport.
|
|
113
|
+
|
|
114
|
+
Session mapping: ACP `sessionId` becomes the Stable Runtime `sessionId`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/adapter-deepagents",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"@langchain/node-vfs": "^0.1.4",
|
|
16
16
|
"@langchain/ollama": "^1.2.7",
|
|
17
17
|
"@langchain/openai": "^1.4.5",
|
|
18
|
-
"@stable-harness/core": "0.0.
|
|
18
|
+
"@stable-harness/core": "0.0.107",
|
|
19
19
|
"deepagents": "^1.10.1",
|
|
20
20
|
"langchain": "^1.4.0"
|
|
21
21
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/adapter-langgraph",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
"types": "dist/src/index.d.ts",
|
|
12
12
|
"peerDependencies": {
|
|
13
13
|
"@langchain/langgraph": "^1.3.0",
|
|
14
|
-
"@stable-harness/core": "0.0.
|
|
14
|
+
"@stable-harness/core": "0.0.107"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
".": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@stable-harness/governance": "0.0.
|
|
15
|
-
"@stable-harness/memory": "0.0.
|
|
14
|
+
"@stable-harness/governance": "0.0.107",
|
|
15
|
+
"@stable-harness/memory": "0.0.107"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -4,4 +4,23 @@ export type AgentProtocolServerOptions = {
|
|
|
4
4
|
baseUrl?: string;
|
|
5
5
|
enabledProtocols?: Array<"acp" | "a2a" | "agui">;
|
|
6
6
|
};
|
|
7
|
+
type JsonRpcMessage = {
|
|
8
|
+
jsonrpc?: string;
|
|
9
|
+
id?: string | number | null;
|
|
10
|
+
method?: string;
|
|
11
|
+
params?: unknown;
|
|
12
|
+
};
|
|
7
13
|
export declare function createAgentProtocolHttpServer(runtime: StableHarnessRuntime, options?: AgentProtocolServerOptions): import("node:http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
14
|
+
export declare function handleAcpJsonRpcMessage(runtime: StableHarnessRuntime, message: JsonRpcMessage): Promise<{
|
|
15
|
+
jsonrpc: string;
|
|
16
|
+
id: string | number | null | undefined;
|
|
17
|
+
result: unknown;
|
|
18
|
+
} | {
|
|
19
|
+
jsonrpc: string;
|
|
20
|
+
id: string | number | null | undefined;
|
|
21
|
+
error: {
|
|
22
|
+
code: number;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
} | undefined>;
|
|
26
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createServer as e}from"node:http";export function createAgentProtocolHttpServer(t,a={}){const r=new Set(a.enabledProtocols??["acp","a2a","agui"]);return e(async(e,s)=>{try{if("GET"===e.method&&"/health"===e.url)return void sendJson(s,200,{ok:!0,protocols:[...r]});if(r.has("a2a")&&await async function handleA2a(e,t,a,r){if("GET"===t.method&&("/.well-known/agent-card.json"===t.url||"/a2a/agent-card.json"===t.url))return sendJson(a,200,function createAgentCard(e,t){const a=e.inspect();return{protocolVersion:"1.0",name:a.workspaceRoot.split(/[\\/]/u).filter(Boolean).at(-1)??"stable-harness",description:"Stable Harness runtime agent endpoint",version:"0.0.0",url:t.baseUrl?`${t.baseUrl.replace(/\/$/u,"")}/a2a`:"/a2a",preferredTransport:"HTTP+JSON",capabilities:{streaming:!0},defaultInputModes:["text/plain"],defaultOutputModes:["text/plain"],skills:a.agents.map(e=>({id:e,name:e,description:e}))}}(e,r)),!0;const s=matchPath(t.url,/^\/a2a\/tasks\/([^/]+)$/u);if("GET"===t.method&&s){const t=e.inspectRequest(s);return sendJson(a,t?200:404,t?{task:toA2aTask(t)}:{error:"task_not_found"}),!0}if("GET"===t.method&&"/a2a/tasks"===t.url)return sendJson(a,200,{tasks:e.listRequests().map(toA2aTaskSummary)}),!0;const n=matchPath(t.url,/^\/a2a\/tasks\/([^/]+):cancel$/u);return"POST"===t.method&&n?(e.cancel(n,"a2a_cancel"),sendJson(a,200,{task:e.inspectRequest(n)}),!0):"POST"===t.method&&"/a2a/message:send"===t.url?(sendJson(a,200,{task:toA2aTask(await e.request(toA2aRuntimeRequest(await readJson(t))))}),!0):"POST"===t.method&&"/a2a/message:stream"===t.url?(await streamA2a(e,await readJson(t),a),!0):"POST"===t.method&&"/a2a/rpc"===t.url&&(await async function handleA2aRpc(e,t,a){if("SendMessage"===t.method){const r=await e.request(toA2aRuntimeRequest(readRecord(t.params)??{}));return void sendJson(a,200,jsonRpcResult(t.id,{task:toA2aTask(r)}))}if("SendStreamingMessage"!==t.method){if("GetTask"===t.method){const r=readString(readRecord(t.params)?.id),s=r?e.inspectRequest(r):void 0;return void sendJson(a,s?200:404,s?jsonRpcResult(t.id,toA2aTask(s)):jsonRpcError(t.id,-32004,"task_not_found"))}"ListTasks"!==t.method?sendJson(a,404,jsonRpcError(t.id,-32601,"method_not_found")):sendJson(a,200,jsonRpcResult(t.id,{tasks:e.listRequests().map(toA2aTaskSummary)}))}else await streamA2a(e,readRecord(t.params)??{},a,t.id)}(e,await readJson(t),a),!0)}(t,e,s,a))return;if(r.has("agui")&&await async function handleAgui(e,t,a){return"GET"===t.method&&"/ag-ui/capabilities"===t.url?(sendJson(a,200,{protocol:"ag-ui",transports:["http+sse"],events:["RunStarted","TextMessageChunk","Custom","RunFinished","RunError"]}),!0):"POST"===t.method&&"/ag-ui/runs"===t.url&&(await async function streamAgui(e,t,a){const r=readRuntimeRequestExtension(t),s=r?.requestId??readString(t.runId)??crypto.randomUUID(),n=r?.sessionId??readString(t.threadId)??readString(t.sessionId)??`thread-${s}`,o=readPromptText(t);writeSseHeaders(a),writeSse(a,{type:"RunStarted",threadId:n,runId:s,input:{messages:t.messages,input:o}});const d=e.subscribe(e=>{e.requestId===s&&writeSse(a,function toAguiEvent(e){return"runtime.progress.narration"===e.type?{type:"Custom",name:"stable-harness.progress",value:e}:"runtime.tool.direct.started"===e.type?{type:"ToolCallStart",toolCallId:`${e.requestId}:${e.toolId}`,toolCallName:e.toolId}:"runtime.tool.direct.completed"===e.type?{type:"ToolCallResult",messageId:`${e.requestId}-message`,toolCallId:`${e.requestId}:${e.toolId}`,content:e.output,role:"tool"}:{type:"Custom",name:`stable-harness.${e.type}`,value:e}}(e))});try{const d=await e.request(r??{input:o,requestId:s,sessionId:n,agentId:readString(t.agentId),metadata:{protocol:"ag-ui"}}),i=`${s}-message`;d.output&&writeSse(a,{type:"TextMessageChunk",messageId:i,role:"assistant",delta:d.output}),writeSse(a,{type:"RunFinished",threadId:n,runId:s,outcome:{type:"completed"===d.state?"success":"interrupt"},result:d.output})}catch(e){writeSse(a,{type:"RunError",threadId:n,runId:s,message:errorMessage(e)})}finally{d(),a.end()}}(e,await readJson(t),a),!0)}(t,e,s))return;if(r.has("acp")&&await async function handleAcp(e,t,a){if("GET"===t.method&&"/acp/capabilities"===t.url)return sendJson(a,200,{protocolVersion:1,agentCapabilities:{loadSession:!0,promptCapabilities:{embeddedContext:!0},_meta:{transport:"http-jsonrpc"}},agentInfo:{name:"stable-harness",title:"Stable Harness",version:"0.0.0"}}),!0;if("POST"===t.method&&"/acp"===t.url){const r=await readJson(t),s=await async function handleAcpMessage(e,t){if(!t.id&&"session/cancel"===t.method){const a=readString(readRecord(t.params)?.sessionId);return void(a&&e.listRequests({sessionId:a,state:"running"}).forEach(t=>e.cancel(t.requestId,"acp_cancel")))}if("initialize"===t.method)return jsonRpcResult(t.id,{protocolVersion:1,agentCapabilities:{loadSession:!0,promptCapabilities:{embeddedContext:!0},_meta:{transport:"http-jsonrpc"}},agentInfo:{name:"stable-harness",title:"Stable Harness",version:"0.0.0"}});if("session/new"===t.method)return jsonRpcResult(t.id,{sessionId:`acp-${crypto.randomUUID()}`});if("session/load"===t.method)return jsonRpcResult(t.id,{sessionId:readString(readRecord(t.params)?.sessionId)});if("session/list"===t.method)return jsonRpcResult(t.id,{sessions:e.listSessions()});if("session/prompt"===t.method){const a=readRecord(t.params)??{},r=readRuntimeRequestExtension(a),s=await e.request(r??{input:readPromptText(a),sessionId:readString(a.sessionId),agentId:readString(a.agentId),metadata:{protocol:"acp",transport:"http-jsonrpc"}});return jsonRpcResult(t.id,{stopReason:"completed"===s.state?"end_turn":"refusal",_meta:{requestId:s.requestId,output:s.output}})}return jsonRpcError(t.id,-32601,"method_not_found")}(e,r);return s?sendJson(a,200,s):a.writeHead(204).end(),!0}return!1}(t,e,s))return;sendJson(s,404,{error:"not_found"})}catch(e){sendJson(s,400,{error:errorMessage(e)})}})}async function streamA2a(e,t,a,r){const s=toA2aRuntimeRequest(t);writeSseHeaders(a);const n=e.subscribe(e=>{e.requestId===s.requestId&&writeSse(a,{jsonrpc:"2.0",id:r,result:{event:toA2aEvent(e)}})});try{const t=await e.request(s);writeSse(a,{jsonrpc:"2.0",id:r,result:{task:toA2aTask(t),final:!0}})}finally{n(),a.end()}}function toA2aRuntimeRequest(e){const t=readRuntimeRequestExtension(e);if(t)return{...t,metadata:{...t.metadata,protocol:"a2a"}};const a=readRecord(e.message)??e;return{input:readPromptText(a),requestId:readString(e.requestId)??readString(a.messageId),sessionId:readString(e.taskId)??readString(a.taskId)??readString(a.contextId),agentId:readString(e.agentId)??readString(e.metadata&&readRecord(e.metadata)?.agentId),metadata:{protocol:"a2a",configuration:e.configuration,metadata:e.metadata}}}function toA2aTask(e){const t="state"in e?e.state:e.summary.state,a="output"in e?e.output:e.output??"",r="requestId"in e?e.requestId:e.summary.requestId;return{id:r,contextId:"sessionId"in e?e.sessionId:e.summary.sessionId,status:{state:"completed"===t?"completed":t,message:a?{role:"agent",messageId:`${r}-response`,parts:[{kind:"text",text:a}]}:void 0}}}function toA2aTaskSummary(e){return{id:e.requestId,contextId:e.sessionId,status:{state:e.state},metadata:{agentId:e.agentId}}}function toA2aEvent(e){return{kind:"status-update",taskId:e.requestId,contextId:e.sessionId,status:{state:e.type},metadata:e}}function readRuntimeRequestExtension(e){const t=readRecord(e.runtimeRequest)??readRecord(readRecord(e.metadata)?.runtimeRequest);if(t)return{input:readString(t.input)??"",...readString(t.requestId)?{requestId:readString(t.requestId)}:{},...readString(t.sessionId)?{sessionId:readString(t.sessionId)}:{},...readString(t.agentId)?{agentId:readString(t.agentId)}:{},...readToolCall(t.toolCall)?{toolCall:readToolCall(t.toolCall)}:{},...readWorkflow(t.workflow)?{workflow:readWorkflow(t.workflow)}:{},...readRecord(t.metadata)?{metadata:readRecord(t.metadata)}:{}}}function readToolCall(e){const t=readRecord(e),a=readString(t?.toolId);return a?{toolId:a,args:t?.args}:void 0}function readWorkflow(e){const t=readRecord(e);if(t)return{...readString(t.workflowId)?{workflowId:readString(t.workflowId)}:{},...readString(t.routeId)?{routeId:readString(t.routeId)}:{},...void 0!==t.input?{input:t.input}:{},...readRecord(t.metadata)?{metadata:readRecord(t.metadata)}:{}}}function readPromptText(e){return"string"==typeof e.input?e.input:"string"==typeof e.text?e.text:"string"==typeof e.prompt?e.prompt:Array.isArray(e.parts)?e.parts.map(readPartText).filter(Boolean).join("\n"):Array.isArray(e.messages)?readPromptText(readRecord(e.messages.at(-1))??{}):Array.isArray(e.content)?e.content.map(readPartText).filter(Boolean).join("\n"):"string"==typeof e.content?e.content:""}function readPartText(e){const t=readRecord(e);return t?readString(t.text)??readString(t.content)??"":"string"==typeof e?e:""}function matchPath(e,t){const a=(e??"").match(t);return a?.[1]?decodeURIComponent(a[1]):void 0}function readRecord(e){return"object"!=typeof e||null===e||Array.isArray(e)?void 0:e}function readString(e){return"string"==typeof e&&e.trim()?e:void 0}function jsonRpcResult(e,t){return{jsonrpc:"2.0",id:e,result:t}}function jsonRpcError(e,t,a){return{jsonrpc:"2.0",id:e,error:{code:t,message:a}}}function sendJson(e,t,a){e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(a))}function writeSseHeaders(e){e.writeHead(200,{"content-type":"text/event-stream","cache-control":"no-cache",connection:"keep-alive"})}function writeSse(e,t){e.write(`data: ${JSON.stringify(t)}\n\n`)}async function readJson(e){const t=[];for await(const a of e)t.push(Buffer.isBuffer(a)?a:Buffer.from(a));return t.length>0?JSON.parse(Buffer.concat(t).toString("utf8")):{}}function errorMessage(e){return e instanceof Error?e.message:String(e)}
|
|
1
|
+
import{createServer as e}from"node:http";export function createAgentProtocolHttpServer(t,a={}){const r=new Set(a.enabledProtocols??["acp","a2a","agui"]);return e(async(e,s)=>{try{if("GET"===e.method&&"/health"===e.url)return void sendJson(s,200,{ok:!0,protocols:[...r]});if("GET"===e.method&&"/capabilities"===e.url)return void sendJson(s,200,function createProtocolCapabilityManifest(e){return{protocol:"stable-harness-agent-protocols",protocols:[...e].sort().map(e=>({id:e,transports:"acp"===e?["stdio","http-jsonrpc"]:"a2a"===e?["http-json","sse"]:["http-sse"],session:"acp"===e?{level:"protocol-session",mapsTo:"sessionId",operatorApi:!1}:"a2a"===e?{level:"continuity",mapsTo:"contextId|taskId",operatorApi:!1}:{level:"continuity",mapsTo:"threadId|sessionId",operatorApi:!1}})),stableRuntime:{session:{level:"operator-api",operatorApi:!0}}}}(r));if(r.has("a2a")&&await async function handleA2a(e,t,a,r){if("GET"===t.method&&("/.well-known/agent-card.json"===t.url||"/a2a/agent-card.json"===t.url))return sendJson(a,200,function createAgentCard(e,t){const a=e.inspect();return{protocolVersion:"1.0",name:a.workspaceRoot.split(/[\\/]/u).filter(Boolean).at(-1)??"stable-harness",description:"Stable Harness runtime agent endpoint",version:"0.0.0",url:t.baseUrl?`${t.baseUrl.replace(/\/$/u,"")}/a2a`:"/a2a",preferredTransport:"HTTP+JSON",capabilities:{streaming:!0},defaultInputModes:["text/plain"],defaultOutputModes:["text/plain"],skills:a.agents.map(e=>({id:e,name:e,description:e}))}}(e,r)),!0;const s=matchPath(t.url,/^\/a2a\/tasks\/([^/]+)$/u);if("GET"===t.method&&s){const t=e.inspectRequest(s);return sendJson(a,t?200:404,t?{task:toA2aTask(t)}:{error:"task_not_found"}),!0}if("GET"===t.method&&"/a2a/tasks"===t.url)return sendJson(a,200,{tasks:e.listRequests().map(toA2aTaskSummary)}),!0;const n=matchPath(t.url,/^\/a2a\/tasks\/([^/]+):cancel$/u);return"POST"===t.method&&n?(e.cancel(n,"a2a_cancel"),sendJson(a,200,{task:e.inspectRequest(n)}),!0):"POST"===t.method&&"/a2a/message:send"===t.url?(sendJson(a,200,{task:toA2aTask(await e.request(toA2aRuntimeRequest(await readJson(t))))}),!0):"POST"===t.method&&"/a2a/message:stream"===t.url?(await streamA2a(e,await readJson(t),a),!0):"POST"===t.method&&"/a2a/rpc"===t.url&&(await async function handleA2aRpc(e,t,a){if("SendMessage"===t.method){const r=await e.request(toA2aRuntimeRequest(readRecord(t.params)??{}));return void sendJson(a,200,jsonRpcResult(t.id,{task:toA2aTask(r)}))}if("SendStreamingMessage"!==t.method){if("GetTask"===t.method){const r=readString(readRecord(t.params)?.id),s=r?e.inspectRequest(r):void 0;return void sendJson(a,s?200:404,s?jsonRpcResult(t.id,toA2aTask(s)):jsonRpcError(t.id,-32004,"task_not_found"))}"ListTasks"!==t.method?sendJson(a,404,jsonRpcError(t.id,-32601,"method_not_found")):sendJson(a,200,jsonRpcResult(t.id,{tasks:e.listRequests().map(toA2aTaskSummary)}))}else await streamA2a(e,readRecord(t.params)??{},a,t.id)}(e,await readJson(t),a),!0)}(t,e,s,a))return;if(r.has("agui")&&await async function handleAgui(e,t,a){return"GET"===t.method&&"/ag-ui/capabilities"===t.url?(sendJson(a,200,{protocol:"ag-ui",transports:["http+sse"],events:["RunStarted","TextMessageChunk","Custom","RunFinished","RunError"]}),!0):"POST"===t.method&&"/ag-ui/runs"===t.url&&(await async function streamAgui(e,t,a){const r=readRuntimeRequestExtension(t),s=r?.requestId??readString(t.runId)??crypto.randomUUID(),n=r?.sessionId??readString(t.threadId)??readString(t.sessionId)??`thread-${s}`,o=readPromptText(t);writeSseHeaders(a),writeSse(a,{type:"RunStarted",threadId:n,runId:s,input:{messages:t.messages,input:o}});const d=e.subscribe(e=>{e.requestId===s&&writeSse(a,function toAguiEvent(e){return"runtime.progress.narration"===e.type?{type:"Custom",name:"stable-harness.progress",value:e}:"runtime.tool.direct.started"===e.type?{type:"ToolCallStart",toolCallId:`${e.requestId}:${e.toolId}`,toolCallName:e.toolId}:"runtime.tool.direct.completed"===e.type?{type:"ToolCallResult",messageId:`${e.requestId}-message`,toolCallId:`${e.requestId}:${e.toolId}`,content:e.output,role:"tool"}:{type:"Custom",name:`stable-harness.${e.type}`,value:e}}(e))});try{const d=await e.request(r??{input:o,requestId:s,sessionId:n,agentId:readString(t.agentId),metadata:{protocol:"ag-ui"}}),i=`${s}-message`;d.output&&writeSse(a,{type:"TextMessageChunk",messageId:i,role:"assistant",delta:d.output}),writeSse(a,{type:"RunFinished",threadId:n,runId:s,outcome:{type:"completed"===d.state?"success":"interrupt"},result:d.output})}catch(e){writeSse(a,{type:"RunError",threadId:n,runId:s,message:errorMessage(e)})}finally{d(),a.end()}}(e,await readJson(t),a),!0)}(t,e,s))return;if(r.has("acp")&&await async function handleAcp(e,t,a){if("GET"===t.method&&"/acp/capabilities"===t.url)return sendJson(a,200,{protocolVersion:1,agentCapabilities:{loadSession:!0,promptCapabilities:{embeddedContext:!0},_meta:{transport:"http-jsonrpc"}},agentInfo:{name:"stable-harness",title:"Stable Harness",version:"0.0.0"}}),!0;if("POST"===t.method&&"/acp"===t.url){const r=await readJson(t),s=await handleAcpJsonRpcMessage(e,r);return s?sendJson(a,200,s):a.writeHead(204).end(),!0}return!1}(t,e,s))return;sendJson(s,404,{error:"not_found"})}catch(e){sendJson(s,400,{error:errorMessage(e)})}})}export async function handleAcpJsonRpcMessage(e,t){if(!t.id&&"session/cancel"===t.method){const a=readString(readRecord(t.params)?.sessionId);return void(a&&e.listRequests({sessionId:a,state:"running"}).forEach(t=>e.cancel(t.requestId,"acp_cancel")))}if("initialize"===t.method)return jsonRpcResult(t.id,{protocolVersion:1,agentCapabilities:{loadSession:!0,promptCapabilities:{embeddedContext:!0},_meta:{transport:"http-jsonrpc"}},agentInfo:{name:"stable-harness",title:"Stable Harness",version:"0.0.0"}});if("session/new"===t.method)return jsonRpcResult(t.id,{sessionId:`acp-${crypto.randomUUID()}`});if("session/load"===t.method)return jsonRpcResult(t.id,{sessionId:readString(readRecord(t.params)?.sessionId)});if("session/list"===t.method)return jsonRpcResult(t.id,{sessions:e.listSessions()});if("session/prompt"===t.method){const a=readRecord(t.params)??{},r=readRuntimeRequestExtension(a),s=await e.request(r??{input:readPromptText(a),sessionId:readString(a.sessionId),agentId:readString(a.agentId),metadata:{protocol:"acp",transport:"http-jsonrpc"}});return jsonRpcResult(t.id,{stopReason:"completed"===s.state?"end_turn":"refusal",_meta:{requestId:s.requestId,output:s.output}})}return jsonRpcError(t.id,-32601,"method_not_found")}async function streamA2a(e,t,a,r){const s=toA2aRuntimeRequest(t);writeSseHeaders(a);const n=e.subscribe(e=>{e.requestId===s.requestId&&writeSse(a,{jsonrpc:"2.0",id:r,result:{event:toA2aEvent(e)}})});try{const t=await e.request(s);writeSse(a,{jsonrpc:"2.0",id:r,result:{task:toA2aTask(t),final:!0}})}finally{n(),a.end()}}function toA2aRuntimeRequest(e){const t=readRuntimeRequestExtension(e);if(t)return{...t,metadata:{...t.metadata,protocol:"a2a"}};const a=readRecord(e.message)??e;return{input:readPromptText(a),requestId:readString(e.requestId)??readString(a.messageId),sessionId:readString(e.taskId)??readString(a.taskId)??readString(a.contextId),agentId:readString(e.agentId)??readString(e.metadata&&readRecord(e.metadata)?.agentId),metadata:{protocol:"a2a",configuration:e.configuration,metadata:e.metadata}}}function toA2aTask(e){const t="state"in e?e.state:e.summary.state,a="output"in e?e.output:e.output??"",r="requestId"in e?e.requestId:e.summary.requestId;return{id:r,contextId:"sessionId"in e?e.sessionId:e.summary.sessionId,status:{state:"completed"===t?"completed":t,message:a?{role:"agent",messageId:`${r}-response`,parts:[{kind:"text",text:a}]}:void 0}}}function toA2aTaskSummary(e){return{id:e.requestId,contextId:e.sessionId,status:{state:e.state},metadata:{agentId:e.agentId}}}function toA2aEvent(e){const t="runtime.request.completed"===e.type?{role:"agent",messageId:`${e.requestId}-response`,parts:[{kind:"text",text:e.output}]}:void 0;return{kind:"status-update",taskId:e.requestId,contextId:e.sessionId,status:{state:(a=e.type,"runtime.request.started"===a?"working":"runtime.request.completed"===a?"completed":"runtime.request.failed"===a?"failed":"runtime.request.cancelled"===a?"canceled":"working"),message:t},metadata:{stableHarnessEvent:e}};var a}function readRuntimeRequestExtension(e){const t=readRecord(e.runtimeRequest)??readRecord(readRecord(e.metadata)?.runtimeRequest);if(t)return{input:readString(t.input)??"",...readString(t.requestId)?{requestId:readString(t.requestId)}:{},...readString(t.sessionId)?{sessionId:readString(t.sessionId)}:{},...readString(t.agentId)?{agentId:readString(t.agentId)}:{},...readToolCall(t.toolCall)?{toolCall:readToolCall(t.toolCall)}:{},...readWorkflow(t.workflow)?{workflow:readWorkflow(t.workflow)}:{},...readRecord(t.metadata)?{metadata:readRecord(t.metadata)}:{}}}function readToolCall(e){const t=readRecord(e),a=readString(t?.toolId);return a?{toolId:a,args:t?.args}:void 0}function readWorkflow(e){const t=readRecord(e);if(t)return{...readString(t.workflowId)?{workflowId:readString(t.workflowId)}:{},...readString(t.routeId)?{routeId:readString(t.routeId)}:{},...void 0!==t.input?{input:t.input}:{},...readRecord(t.metadata)?{metadata:readRecord(t.metadata)}:{}}}function readPromptText(e){return"string"==typeof e.input?e.input:"string"==typeof e.text?e.text:"string"==typeof e.prompt?e.prompt:Array.isArray(e.parts)?e.parts.map(readPartText).filter(Boolean).join("\n"):Array.isArray(e.messages)?readPromptText(readRecord(e.messages.at(-1))??{}):Array.isArray(e.content)?e.content.map(readPartText).filter(Boolean).join("\n"):"string"==typeof e.content?e.content:""}function readPartText(e){const t=readRecord(e);return t?readString(t.text)??readString(t.content)??"":"string"==typeof e?e:""}function matchPath(e,t){const a=(e??"").match(t);return a?.[1]?decodeURIComponent(a[1]):void 0}function readRecord(e){return"object"!=typeof e||null===e||Array.isArray(e)?void 0:e}function readString(e){return"string"==typeof e&&e.trim()?e:void 0}function jsonRpcResult(e,t){return{jsonrpc:"2.0",id:e,result:t}}function jsonRpcError(e,t,a){return{jsonrpc:"2.0",id:e,error:{code:t,message:a}}}function sendJson(e,t,a){e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(a))}function writeSseHeaders(e){e.writeHead(200,{"content-type":"text/event-stream","cache-control":"no-cache",connection:"keep-alive"})}function writeSse(e,t){e.write(`data: ${JSON.stringify(t)}\n\n`)}async function readJson(e){const t=[];for await(const a of e)t.push(Buffer.isBuffer(a)?a:Buffer.from(a));return t.length>0?JSON.parse(Buffer.concat(t).toString("utf8")):{}}function errorMessage(e){return e instanceof Error?e.message:String(e)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createInProcessClient } from "./in-process-client.js";
|
|
2
|
-
export { createAgentProtocolHttpServer } from "./agent-protocols.js";
|
|
2
|
+
export { createAgentProtocolHttpServer, handleAcpJsonRpcMessage } from "./agent-protocols.js";
|
|
3
3
|
export type { AgentProtocolServerOptions } from "./agent-protocols.js";
|
|
4
4
|
export { createHttpServer } from "./http-server.js";
|
|
5
5
|
export { createOpenAiCompatibleHttpServer } from "./openai-compatible.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{createInProcessClient}from"./in-process-client.js";export{createAgentProtocolHttpServer}from"./agent-protocols.js";export{createHttpServer}from"./http-server.js";export{createOpenAiCompatibleHttpServer}from"./openai-compatible.js";
|
|
1
|
+
export{createInProcessClient}from"./in-process-client.js";export{createAgentProtocolHttpServer,handleAcpJsonRpcMessage}from"./agent-protocols.js";export{createHttpServer}from"./http-server.js";export{createOpenAiCompatibleHttpServer}from"./openai-compatible.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/protocols",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
"main": "dist/src/index.js",
|
|
11
11
|
"types": "dist/src/index.d.ts",
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@stable-harness/core": "0.0.
|
|
13
|
+
"@stable-harness/core": "0.0.107"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/workspace-yaml",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
".": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@stable-harness/core": "0.0.
|
|
14
|
+
"@stable-harness/core": "0.0.107"
|
|
15
15
|
}
|
|
16
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stable-harness",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Stable application runtime and operator control plane for agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -82,14 +82,14 @@
|
|
|
82
82
|
"@langchain/node-vfs": "^0.1.4",
|
|
83
83
|
"@langchain/ollama": "^1.2.7",
|
|
84
84
|
"@langchain/openai": "^1.4.5",
|
|
85
|
-
"@stable-harness/adapter-deepagents": "0.0.
|
|
86
|
-
"@stable-harness/adapter-langgraph": "0.0.
|
|
87
|
-
"@stable-harness/core": "0.0.
|
|
88
|
-
"@stable-harness/governance": "0.0.
|
|
89
|
-
"@stable-harness/memory": "0.0.
|
|
90
|
-
"@stable-harness/protocols": "0.0.
|
|
91
|
-
"@stable-harness/tool-gateway": "0.0.
|
|
92
|
-
"@stable-harness/workspace-yaml": "0.0.
|
|
85
|
+
"@stable-harness/adapter-deepagents": "0.0.107",
|
|
86
|
+
"@stable-harness/adapter-langgraph": "0.0.107",
|
|
87
|
+
"@stable-harness/core": "0.0.107",
|
|
88
|
+
"@stable-harness/governance": "0.0.107",
|
|
89
|
+
"@stable-harness/memory": "0.0.107",
|
|
90
|
+
"@stable-harness/protocols": "0.0.107",
|
|
91
|
+
"@stable-harness/tool-gateway": "0.0.107",
|
|
92
|
+
"@stable-harness/workspace-yaml": "0.0.107",
|
|
93
93
|
"deepagents": "^1.10.1",
|
|
94
94
|
"langchain": "^1.4.0",
|
|
95
95
|
"yaml": "^2.8.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/adapter-deepagents",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"@langchain/node-vfs": "^0.1.4",
|
|
16
16
|
"@langchain/ollama": "^1.2.7",
|
|
17
17
|
"@langchain/openai": "^1.4.5",
|
|
18
|
-
"@stable-harness/core": "0.0.
|
|
18
|
+
"@stable-harness/core": "0.0.107",
|
|
19
19
|
"deepagents": "^1.10.1",
|
|
20
20
|
"langchain": "^1.4.0"
|
|
21
21
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/adapter-langgraph",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
"types": "dist/src/index.d.ts",
|
|
12
12
|
"peerDependencies": {
|
|
13
13
|
"@langchain/langgraph": "^1.3.0",
|
|
14
|
-
"@stable-harness/core": "0.0.
|
|
14
|
+
"@stable-harness/core": "0.0.107"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type CliArgs = {
|
|
2
2
|
workspaceRoot: string;
|
|
3
|
-
command: "request" | "console" | "start" | "stop" | "init" | "build";
|
|
3
|
+
command: "request" | "console" | "start" | "stop" | "init" | "build" | "acp-stdio";
|
|
4
4
|
buildTarget?: "docker";
|
|
5
5
|
buildOutput?: string;
|
|
6
6
|
workflowRenderId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function parseArgs(e){const r=function createDefaultArgs(){return{workspaceRoot:process.cwd(),command:"request",toolArgs:void 0,trace:!1,traceJson:!1,progress:!1,serveOpenAi:!1,host:process.env.STABLE_HARNESS_OPENAI_HOST,port:process.env.STABLE_HARNESS_OPENAI_PORT?Number(process.env.STABLE_HARNESS_OPENAI_PORT):void 0,apiKey:process.env.STABLE_HARNESS_OPENAI_API_KEY,runtimeMode:readRuntimeMode(process.env.STABLE_HARNESS_CLIENT_RUNTIME)??"auto",daemonUrl:process.env.STABLE_HARNESS_DAEMON_URL,clientProtocol:readClientProtocol(process.env.STABLE_HARNESS_CLIENT_PROTOCOL)??"stable-runtime",protocolUrl:process.env.STABLE_HARNESS_PROTOCOL_URL,timeoutMs:Number(process.env.STABLE_HARNESS_CLI_TIMEOUT_MS??3e5),help:!1,prompt:"",shouldRunRequest:!1}}(),t=[];for(let o=0;o<e.length;o+=1)o=parseOneArg(e,o,r,t);return{...r,prompt:t.join(" "),shouldRunRequest:Boolean(r.toolId||r.workflowRunId||t.length>0)}}function parseOneArg(e,r,t,o){const n=function readNextArg(e,r){return{index:r+1,value:e[r+1]}}(e,r);if(0===o.length&&function parseTopLevelCommand(e,r,t){return"start"===e[r]?(t.command="start",t.serveOpenAi=!0,!0):"stop"===e[r]?(t.command="stop",!0):"init"===e[r]?(t.command="init",!0):"build"===e[r]?(t.command="build",!0):"console"===e[r]||"session"===e[r]?(t.command="console",!0):"workflow"!==e[r]||"render"!==e[r+1]&&"inspect"!==e[r+1]?"agent"===e[r]&&"render"===e[r+1]&&(Object.assign(t,function parseAgentCommand(e,r){if("render"===e[r+1])return{index:r+2,agentRenderId:e[r+2]};throw new Error("Usage: stable-harness agent render <agent-id>")}(e,r)),!0):(Object.assign(t,function parseWorkflowCommand(e,r){if("render"===e[r+1])return{index:r+2,workflowRenderId:e[r+2],workflowInspectId:void 0};if("inspect"===e[r+1])return{index:r+2,workflowRenderId:void 0,workflowInspectId:e[r+2]};throw new Error("Usage: stable-harness workflow <render|inspect> <workflow-id>")}(e,r)),!0)}(e,r,t))return function stateIndex(e,r){return"workflow"===e[r]||"agent"===e[r]?r+2:r}(e,r);if("-h"===e[r]||"--help"===e[r])t.help=!0;else if("start"===t.command&&function isProtocolName(e){return"openai"===e||"openai-compatible"===e}(e[r]))t.serveOpenAi=!0;else{if("-w"===e[r]||"--workspace"===e[r])return setString(n,t,"workspaceRoot");if("--agent"===e[r])return setString(n,t,"agentId");if("--workflow"===e[r])return setString(n,t,"workflowRunId");if("--session-id"===e[r])return setString(n,t,"sessionId");if("--tool"===e[r])return setString(n,t,"toolId");if("--tool-args-json"===e[r])return t.toolArgs=function parseJsonArg(e){try{return JSON.parse(e)}catch(e){const r=e instanceof Error?e.message:String(e);throw new Error(`Invalid --tool-args-json value: ${r}`)}}(n.value??"{}"),n.index;if("--trace"===e[r])t.trace=!0;else if("--trace-json"===e[r])t.traceJson=!0;else if("--progress"===e[r])t.progress=!0;else if("--serve-openai"===e[r])t.serveOpenAi=!0;else{if("--host"===e[r])return setString(n,t,"host");if("--port"===e[r])return t.port=Number(n.value??t.port),n.index;if("--api-key"===e[r])return setString(n,t,"apiKey");if("--runtime"===e[r])return function setRuntimeMode(e,r){const t=readRuntimeMode(e.value);if(!t)throw new Error("Unsupported --runtime value. Supported values: auto, daemon, local");return r.runtimeMode=t,e.index}(n,t);if("--daemon-url"===e[r])return setString(n,t,"daemonUrl");if("--protocol"===e[r])return function setClientProtocol(e,r){const t=readClientProtocol(e.value);if(!t)throw new Error("Unsupported --protocol value. Supported values: stable-runtime, a2a, agui, acp");return r.clientProtocol=t,e.index}(n,t);if("--protocol-url"===e[r])return setString(n,t,"protocolUrl");if("--target"===e[r])return function setBuildTarget(e,r){if("docker"===e.value)return r.buildTarget="docker",e.index;throw new Error("Unsupported build target. Supported targets: docker")}(n,t);if("--output"===e[r]||"-o"===e[r])return setString(n,t,"buildOutput");if("--timeout-ms"===e[r])return t.timeoutMs=Number(n.value??t.timeoutMs),n.index;o.push(e[r])}}return r}function setString(e,r,t){return"string"==typeof e.value&&Object.assign(r,{[t]:e.value}),e.index}export function helpText(){return["Usage:"," stable-harness -w <workspace> [--agent <id>] [prompt]"," stable-harness console -w <workspace>"," stable-harness workflow render <workflow-id> -w <workspace>"," stable-harness workflow inspect <workflow-id> -w <workspace>"," stable-harness agent render <agent-id> -w <workspace>"," stable-harness init [workspace]"," stable-harness build --target docker -w <workspace> --output <dir>"," stable-harness start -w <workspace>"," stable-harness stop -w <workspace>","","Options:"," -w, --workspace <path> Workspace root."," --serve-openai Legacy alias for start."," --agent <id> Select an agent for a request."," --workflow <id> Run a configured workflow."," --session-id <id> Attach the request to an existing runtime session."," --tool <id> Invoke an explicit registered tool."," --tool-args-json <json> Tool arguments for --tool."," --trace Print trace lines."," --trace-json Print trace JSON."," --progress Legacy alias; CLI events are controlled by runtime.cli.events."," --runtime <mode> Client runtime mode: auto, daemon, or local."," --daemon-url <url> Stable Runtime daemon URL for daemon/auto modes."," --protocol <name> Client protocol: stable-runtime, a2a, agui, or acp."," --protocol-url <url> Protocol server URL for a2a/agui/acp."," --target docker Build target for workspace artifacts."," -o, --output <dir> Build output directory."," --timeout-ms <ms> Request timeout."," -h, --help Show this help.",""].join("\n")}function readRuntimeMode(e){return"auto"===e||"daemon"===e||"local"===e?e:void 0}function readClientProtocol(e){return"stable"===e||"stable-runtime"===e||"http"===e?"stable-runtime":"a2a"===e||"agui"===e||"acp"===e?e:"ag-ui"===e?"agui":void 0}
|
|
1
|
+
export function parseArgs(e){const r=function createDefaultArgs(){return{workspaceRoot:process.cwd(),command:"request",toolArgs:void 0,trace:!1,traceJson:!1,progress:!1,serveOpenAi:!1,host:process.env.STABLE_HARNESS_OPENAI_HOST,port:process.env.STABLE_HARNESS_OPENAI_PORT?Number(process.env.STABLE_HARNESS_OPENAI_PORT):void 0,apiKey:process.env.STABLE_HARNESS_OPENAI_API_KEY,runtimeMode:readRuntimeMode(process.env.STABLE_HARNESS_CLIENT_RUNTIME)??"auto",daemonUrl:process.env.STABLE_HARNESS_DAEMON_URL,clientProtocol:readClientProtocol(process.env.STABLE_HARNESS_CLIENT_PROTOCOL)??"stable-runtime",protocolUrl:process.env.STABLE_HARNESS_PROTOCOL_URL,timeoutMs:Number(process.env.STABLE_HARNESS_CLI_TIMEOUT_MS??3e5),help:!1,prompt:"",shouldRunRequest:!1}}(),t=[];for(let o=0;o<e.length;o+=1)o=parseOneArg(e,o,r,t);return{...r,prompt:t.join(" "),shouldRunRequest:Boolean(r.toolId||r.workflowRunId||t.length>0)}}function parseOneArg(e,r,t,o){const n=function readNextArg(e,r){return{index:r+1,value:e[r+1]}}(e,r);if(0===o.length&&function parseTopLevelCommand(e,r,t){return"start"===e[r]?(t.command="start",t.serveOpenAi=!0,!0):"stop"===e[r]?(t.command="stop",!0):"init"===e[r]?(t.command="init",!0):"build"===e[r]?(t.command="build",!0):"acp-stdio"===e[r]?(t.command="acp-stdio",!0):"console"===e[r]||"session"===e[r]?(t.command="console",!0):"workflow"!==e[r]||"render"!==e[r+1]&&"inspect"!==e[r+1]?"agent"===e[r]&&"render"===e[r+1]&&(Object.assign(t,function parseAgentCommand(e,r){if("render"===e[r+1])return{index:r+2,agentRenderId:e[r+2]};throw new Error("Usage: stable-harness agent render <agent-id>")}(e,r)),!0):(Object.assign(t,function parseWorkflowCommand(e,r){if("render"===e[r+1])return{index:r+2,workflowRenderId:e[r+2],workflowInspectId:void 0};if("inspect"===e[r+1])return{index:r+2,workflowRenderId:void 0,workflowInspectId:e[r+2]};throw new Error("Usage: stable-harness workflow <render|inspect> <workflow-id>")}(e,r)),!0)}(e,r,t))return function stateIndex(e,r){return"workflow"===e[r]||"agent"===e[r]?r+2:r}(e,r);if("-h"===e[r]||"--help"===e[r])t.help=!0;else if("start"===t.command&&function isProtocolName(e){return"openai"===e||"openai-compatible"===e}(e[r]))t.serveOpenAi=!0;else{if("-w"===e[r]||"--workspace"===e[r])return setString(n,t,"workspaceRoot");if("--agent"===e[r])return setString(n,t,"agentId");if("--workflow"===e[r])return setString(n,t,"workflowRunId");if("--session-id"===e[r])return setString(n,t,"sessionId");if("--tool"===e[r])return setString(n,t,"toolId");if("--tool-args-json"===e[r])return t.toolArgs=function parseJsonArg(e){try{return JSON.parse(e)}catch(e){const r=e instanceof Error?e.message:String(e);throw new Error(`Invalid --tool-args-json value: ${r}`)}}(n.value??"{}"),n.index;if("--trace"===e[r])t.trace=!0;else if("--trace-json"===e[r])t.traceJson=!0;else if("--progress"===e[r])t.progress=!0;else if("--serve-openai"===e[r])t.serveOpenAi=!0;else{if("--host"===e[r])return setString(n,t,"host");if("--port"===e[r])return t.port=Number(n.value??t.port),n.index;if("--api-key"===e[r])return setString(n,t,"apiKey");if("--runtime"===e[r])return function setRuntimeMode(e,r){const t=readRuntimeMode(e.value);if(!t)throw new Error("Unsupported --runtime value. Supported values: auto, daemon, local");return r.runtimeMode=t,e.index}(n,t);if("--daemon-url"===e[r])return setString(n,t,"daemonUrl");if("--protocol"===e[r])return function setClientProtocol(e,r){const t=readClientProtocol(e.value);if(!t)throw new Error("Unsupported --protocol value. Supported values: stable-runtime, a2a, agui, acp");return r.clientProtocol=t,e.index}(n,t);if("--protocol-url"===e[r])return setString(n,t,"protocolUrl");if("--target"===e[r])return function setBuildTarget(e,r){if("docker"===e.value)return r.buildTarget="docker",e.index;throw new Error("Unsupported build target. Supported targets: docker")}(n,t);if("--output"===e[r]||"-o"===e[r])return setString(n,t,"buildOutput");if("--timeout-ms"===e[r])return t.timeoutMs=Number(n.value??t.timeoutMs),n.index;o.push(e[r])}}return r}function setString(e,r,t){return"string"==typeof e.value&&Object.assign(r,{[t]:e.value}),e.index}export function helpText(){return["Usage:"," stable-harness -w <workspace> [--agent <id>] [prompt]"," stable-harness console -w <workspace>"," stable-harness workflow render <workflow-id> -w <workspace>"," stable-harness workflow inspect <workflow-id> -w <workspace>"," stable-harness agent render <agent-id> -w <workspace>"," stable-harness init [workspace]"," stable-harness build --target docker -w <workspace> --output <dir>"," stable-harness acp-stdio -w <workspace>"," stable-harness start -w <workspace>"," stable-harness stop -w <workspace>","","Options:"," -w, --workspace <path> Workspace root."," --serve-openai Legacy alias for start."," --agent <id> Select an agent for a request."," --workflow <id> Run a configured workflow."," --session-id <id> Attach the request to an existing runtime session."," --tool <id> Invoke an explicit registered tool."," --tool-args-json <json> Tool arguments for --tool."," --trace Print trace lines."," --trace-json Print trace JSON."," --progress Legacy alias; CLI events are controlled by runtime.cli.events."," --runtime <mode> Client runtime mode: auto, daemon, or local."," --daemon-url <url> Stable Runtime daemon URL for daemon/auto modes."," --protocol <name> Client protocol: stable-runtime, a2a, agui, or acp."," --protocol-url <url> Protocol server URL for a2a/agui/acp."," --target docker Build target for workspace artifacts."," -o, --output <dir> Build output directory."," --timeout-ms <ms> Request timeout."," -h, --help Show this help.",""].join("\n")}function readRuntimeMode(e){return"auto"===e||"daemon"===e||"local"===e?e:void 0}function readClientProtocol(e){return"stable"===e||"stable-runtime"===e||"http"===e?"stable-runtime":"a2a"===e||"agui"===e||"acp"===e?e:"ag-ui"===e?"agui":void 0}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{realpathSync as e}from"node:fs";import{fileURLToPath as t}from"node:url";import{createBackendModel as r,createDeepAgentsAdapter as o}from"@stable-harness/adapter-deepagents";import{createLangGraphWorkflowAdapter as s}from"@stable-harness/adapter-langgraph";import{createStableHarnessRuntime as i}from"@stable-harness/core";import{projectEvent as n,projectRuntimeTrace as a}from"@stable-harness/core";import{createInMemoryApprovalQueue as u}from"@stable-harness/governance";import{createModuleToolGateway as
|
|
2
|
+
import{realpathSync as e}from"node:fs";import{fileURLToPath as t}from"node:url";import{createBackendModel as r,createDeepAgentsAdapter as o}from"@stable-harness/adapter-deepagents";import{createLangGraphWorkflowAdapter as s}from"@stable-harness/adapter-langgraph";import{createStableHarnessRuntime as i}from"@stable-harness/core";import{projectEvent as n,projectRuntimeTrace as a}from"@stable-harness/core";import{createInMemoryApprovalQueue as u}from"@stable-harness/governance";import{handleAcpJsonRpcMessage as c}from"@stable-harness/protocols";import{createModuleToolGateway as d}from"@stable-harness/tool-gateway";import{loadWorkspaceFromYaml as l}from"@stable-harness/workspace-yaml";import{helpText as p,parseArgs as m}from"./args.js";import{buildWorkspaceArtifact as f}from"./build.js";import{runConsole as w}from"./console/session.js";import{formatCliRuntimeEvent as v,readCliEventViewConfig as g,shouldEnableCliProgressNarration as y}from"./event-view.js";import{initWorkspace as R}from"./init.js";import{runRequestThroughDaemon as h}from"./daemon/client.js";import{ensureCliMemoryServices as I}from"./memory/lifecycle.js";import{createCliMemoryProviders as k}from"./memory/providers.js";import{formatDetail as b,inspectWorkflow as A,renderAgent as q,renderWorkflow as C,workspaceStatus as M}from"./output.js";import{serveProtocol as S,stopProtocol as j}from"./server.js";export async function runCli(e=process.argv.slice(2)){const t=m(e);if(t.help)return void process.stdout.write(p());const r=setTimeout(()=>{process.stderr.write(`stable-harness request timed out after ${t.timeoutMs}ms\n`),process.exit(124)},t.timeoutMs),s=t.workspaceRoot;try{if("init"===t.command)return void process.stdout.write(await R(t.prompt||s));const e=await l(s);if(t.workflowRenderId)return void process.stdout.write(C(e,t.workflowRenderId));if(t.workflowInspectId)return void process.stdout.write(A(e,t.workflowInspectId));if(t.agentRenderId)return void process.stdout.write(q(e,t.agentRenderId));if("build"===t.command)return void process.stdout.write(await f({workspace:e,workspaceRoot:s,outputDir:t.buildOutput,target:t.buildTarget}));if("stop"===t.command)return clearTimeout(r),void await j(e,t);const c=g(e.runtime),createRuntime=()=>async function createCliRuntime(e,t,r){const s=await d({tools:e.tools.values(),options:{betterCall:{mode:"repair"}}}),n=u(),a=await async function createCliMemoryProvidersForCommand(e,t){return"console"===t.command||"acp-stdio"===t.command||t.serveOpenAi||t.shouldRunRequest&&!t.toolId?(await I(e),k(e)):[]}(e,t);let c;return c=i({workspace:e,toolGateway:s,approvals:n,memoryProviders:a,adapters:[o()],workflowAdapters:[createCliWorkflowAdapter(s,()=>c)],progressNarration:y(r,e.runtime)?{enabled:!0,style:"cli"}:void 0,qualityReviewModel:createQualityReviewModel(e)}),c}(e,t,c);if("console"===t.command)return clearTimeout(r),void await w({args:t,runtimePolicy:e.runtime,eventView:c,createRuntime:createRuntime});if("acp-stdio"===t.command)return clearTimeout(r),void await async function runAcpStdio(e){process.stdin.setEncoding("utf8");let t="";for await(const r of process.stdin){t+=String(r);const o=t.split(/\r?\n/u);t=o.pop()??"";for(const t of o)await handleAcpStdioLine(e,t)}t.trim()&&await handleAcpStdioLine(e,t)}(await createRuntime());if(t.shouldRunRequest&&!t.serveOpenAi&&await h({args:t,runtimePolicy:e.runtime,eventView:c}))return;t.shouldRunRequest&&!t.serveOpenAi&&"auto"===t.runtimeMode&&process.stderr.write("stable-harness runtime: no matching daemon found; running in-process\n"),t.shouldRunRequest&&!t.serveOpenAi&&"local"===t.runtimeMode&&process.stderr.write("stable-harness runtime: local mode selected; running in-process\n");const p=await createRuntime();if(t.serveOpenAi)return clearTimeout(r),void await S(p,t);if(!t.shouldRunRequest)return void process.stdout.write(M(e,s));await async function runInProcessRequest(e,t,r){t.trace&&e.subscribe(e=>{const t=n(e);t&&process.stdout.write(`trace:${t.agentId}:${t.label}${b(t.detail)}\n`)}),e.subscribe(e=>{const t=v(e,r);t&&process.stdout.write(`${t}\n`)});const o=await e.request({input:t.prompt,agentId:t.agentId,sessionId:t.sessionId,toolCall:t.toolId?{toolId:t.toolId,args:t.toolArgs}:void 0,workflow:t.workflowRunId?{workflowId:t.workflowRunId,input:t.prompt}:void 0});if(t.trace||t.traceJson){const r=e.getRun(o.requestId),s=r?a(r):[];t.traceJson&&process.stdout.write(`${JSON.stringify({trace:s})}\n`)}process.stdout.write(`${o.output}\n`)}(p,t,c)}finally{clearTimeout(r)}}async function handleAcpStdioLine(e,t){if(!t.trim())return;const r=await c(e,JSON.parse(t));r&&process.stdout.write(`${JSON.stringify(r)}\n`)}function createQualityReviewModel(e){const t=function readQualityModelRef(e){const t=isRecord(e)?e:{},r=isRecord(t.reviewer)?t.reviewer:t;return"string"==typeof r.modelRef&&r.modelRef.trim()?r.modelRef.trim():void 0}(e.runtime.quality),o=t?e.models.get(t):void 0,s=o?r(o):void 0;return function isQualityReviewModel(e){return isRecord(e)&&"function"==typeof e.invoke}(s)?s:void 0}function isRecord(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function createCliWorkflowAdapter(e,t){return s({nodeResolvers:{tools:async({id:t,node:r,request:o,requestId:s,sessionId:i,state:n,workspace:a})=>{return(await e.invoke({toolId:t,args:(u=r.config,c=o.input,d=n.outputs,!0===u?.inputFromState?{...u,requestInput:c,outputs:d}:u&&"requiredInput"in u?u.requiredInput:u&&("args"in u||"cwd"in u||"timeoutMs"in u)?u:"object"==typeof c&&null!==c?c:{}),context:{workspaceRoot:a.root,requestId:s,sessionId:i,agentId:`workflow:${r.id}`,approvalIds:readApprovalIds(o.metadata)}})).output;var u,c,d},agents:async({id:e,node:r,request:o,sessionId:s,state:i})=>{var n,a,u,c;return(await t().request({input:(n=e,a=o.input,u=i.outputs,c=r.config,[`Workflow node agents.${n}: synthesize the workflow evidence into the requested final output.`,`Original request: ${"string"==typeof a?a:JSON.stringify(a)}`,"Requirements:","- Produce the final answer now; do not ask follow-up questions.","- Match the original request language unless workflow config explicitly says otherwise.","- Use only the workflow outputs as evidence; call out uncertainty directly.",...c?[`Workflow node config: ${JSON.stringify(c)}`]:[],"Prior workflow outputs:",JSON.stringify(u)].join("\n")),agentId:e,sessionId:s,metadata:o.metadata})).output}}})}function readApprovalIds(e){const t=e?.approvalIds??e?.approvalId;return"string"==typeof t&&t.trim()?[t.trim()]:Array.isArray(t)?t.filter(e=>"string"==typeof e&&e.trim().length>0):void 0}(function isCliEntrypoint(){const r=process.argv[1];if(!r)return!1;try{return e(t(import.meta.url))===e(r)}catch{return!1}})()&&runCli().catch(e=>{process.stderr.write(`${e instanceof Error?e.message:String(e)}\n`),process.exitCode=1});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as e}from"node:crypto";import{createInterface as s}from"node:readline/promises";import{stdin as t,stdout as n}from"node:process";import{agentProtocolBaseUrl as o,daemonBaseUrl as r,isDaemonAvailable as i,sendAgentProtocolRuntimeRequest as a}from"../daemon/client.js";import{formatCliRuntimeEvent as c}from"../event-view.js";export async function runConsole(l){const u=await async function createConsoleClient(e){if("stable-runtime"!==e.args.clientProtocol){if("local"===e.args.runtimeMode)throw new Error(`stable-harness console: protocol ${e.args.clientProtocol} requires a protocol server; use --protocol stable-runtime for local mode`);return await async function createAgentProtocolConsoleClient(e,s){const t=e.clientProtocol,n=o(s,e.protocolUrl);if("stable-runtime"===t)throw new Error("stable-runtime is handled by the native console client");if(!(await fetch(`${n}/health`,{signal:AbortSignal.timeout(300)})).ok)throw new Error(`stable-harness console: protocol ${t} unavailable at ${n}`);return{mode:"protocol",protocol:t,baseUrl:n,request:e=>a(n,t,e),health:async()=>"ok",sessions:unsupportedList("sessions",t),requests:unsupportedList("requests",t),inspectRequest:async()=>{},memories:unsupportedList("memory",t),clearSession:unsupportedNumber("clear",t),debug:async()=>({protocol:t,baseUrl:n,note:"Protocol console exposes prompt turns only. Use --protocol stable-runtime for operator inspection."}),stream:()=>({close:()=>{}})}}(e.args,e.runtimePolicy)}if("local"===e.args.runtimeMode)return createLocalConsoleClient(await e.createRuntime(),e.eventView);const s=r(e.runtimePolicy,e.args.daemonUrl);if(await i(s,e.args.workspaceRoot))return function createDaemonConsoleClient(e,s){return{mode:"daemon",baseUrl:e,request:s=>async function postJson(e,s){const t=await fetch(e,{method:"POST",body:JSON.stringify(s)});if(!t.ok)throw new Error(`HTTP ${t.status}`);return await t.json()}(`${e}/requests`,s),health:async()=>(await getJson(`${e}/health`)).ok?"ok":"unhealthy",sessions:()=>getJson(`${e}/sessions`),requests:s=>getJson(`${e}/requests${s?`?sessionId=${encodeURIComponent(s)}`:""}`),inspectRequest:async s=>{const t=await fetch(`${e}/requests/${encodeURIComponent(s)}`);return t.ok?await t.json():void 0},memories:s=>getJson(`${e}/memories${s?`?namespace=${encodeURIComponent(s)}`:""}`),clearSession:async s=>(await async function deleteJson(e){const s=await fetch(e,{method:"DELETE"});if(!s.ok)throw new Error(`HTTP ${s.status}`);return await s.json()}(`${e}/sessions/${encodeURIComponent(s)}`)).deletedCount,debug:()=>getJson(`${e}/inspect`),stream:(t,n)=>function streamDaemonEvents(e,s,t,n){const o=new AbortController;return fetch(`${e}/events?requestId=${encodeURIComponent(s)}`,{signal:o.signal}).then(async e=>{const s=e.body?.getReader();if(!s)return;let o="";for(;;){const{value:e,done:r}=await s.read();if(r)return;o+=Buffer.from(e).toString("utf8");const i=o.split("\n\n");o=i.pop()??"";for(const e of i){const s=e.split("\n").find(e=>e.startsWith("data: "))?.slice(6);if(!s)continue;const o=c(JSON.parse(s),t);o&&n(o)}}}).catch(e=>{o.signal.aborted||process.stderr.write(`stable-harness console event stream failed: ${String(e)}\n`)}),{close:()=>o.abort()}}(e,t,s,n)}}(s,e.eventView);if("daemon"===e.args.runtimeMode)throw new Error(`stable-harness console: daemon required but unavailable at ${s}`);return createLocalConsoleClient(await e.createRuntime(),e.eventView)}(l);let d=l.args.sessionId??e(),m=l.args.agentId;process.stderr.write(`stable-harness console: ${function consoleRuntimeMessage(e,s){return"daemon"===e.mode?`connected to daemon at ${e.baseUrl}`:"protocol"===e.mode?`connected through ${e.protocol} at ${e.baseUrl}`:"local"===s?"local mode selected; running in-process":"no matching daemon found; running in-process"}(u,l.args.runtimeMode)}\n`),process.stdout.write(`session ${d}\n`);const p=s({input:t,output:n,terminal:process.stdout.isTTY});try{const e=process.stdout.isTTY?await async function runInteractiveLoop(e){let{sessionId:s,agentId:t}=e;for(;;){const n=await e.rl.question("stable> "),o=await handleConsoleLineSafely({client:e.client,line:n.trim(),sessionId:s,agentId:t,eventView:e.eventView});if(o.done)return o;s=o.sessionId,t=o.agentId}}({rl:p,client:u,sessionId:d,agentId:m,eventView:l.eventView}):await async function runPipedLoop(e){let{sessionId:s,agentId:t}=e;for await(const n of e.rl){const o=await handleConsoleLineSafely({client:e.client,line:n.trim(),sessionId:s,agentId:t,eventView:e.eventView});if(o.done)return o;s=o.sessionId,t=o.agentId}return{done:!0,sessionId:s,agentId:t}}({rl:p,client:u,sessionId:d,agentId:m,eventView:l.eventView});d=e.sessionId,m=e.agentId}catch(e){if(!function isEndOfInput(e){return e instanceof Error&&e.message.includes("closed")}(e))throw e}finally{p.close()}}async function handleConsoleLineSafely(s){try{return await async function handleConsoleLine(s){const t=function parseConsoleCommand(e){if(!e.startsWith("/"))return;const[s="",...t]=e.slice(1).trim().split(/\s+/u);return{name:s,args:t.join(" ").trim()}}(s.line);return s.line?t?await async function runConsoleCommand(s){const{client:t,command:n}=s;if("exit"===n.name||"quit"===n.name)return{...s,done:!0};if("help"===n.name)!function printHelp(){process.stdout.write(["/help Show console commands.","/session [id] Show or switch session.","/new Create and switch to a new session.","/sessions List known sessions.","/agent [id] Show or switch selected agent.","/tool <id> [json] Invoke a tool in the current session.","/requests [all] List current-session or all requests.","/memory [namespace] List memory records.","/clear Delete current session requests.","/health Check runtime health.","/debug Print runtime inspection JSON.","/exit Leave console.",""].join("\n"))}();else if("health"===n.name)process.stdout.write(`${await t.health()}\n`);else if("debug"===n.name)process.stdout.write(`${JSON.stringify(await t.debug())}\n`);else{if("session"===n.name)return switchSession(s,n.args);if("new"===n.name)return switchSession(s,e());if("agent"===n.name)return function switchAgent(e,s){const t=s.trim()||void 0;return process.stdout.write(`agent ${t??"default"}\n`),{done:!1,sessionId:e.sessionId,agentId:t}}(s,n.args);"sessions"===n.name?printJsonLines(await t.sessions(),"sessionId"):"requests"===n.name?printJsonLines(await t.requests("all"===n.args?void 0:s.sessionId),"requestId"):"memory"===n.name||"memories"===n.name?printJsonLines(await t.memories(n.args||void 0),"id"):"clear"===n.name?process.stdout.write(`cleared ${await t.clearSession(s.sessionId)} requests from ${s.sessionId}\n`):"tool"===n.name?await async function runConsoleTool(e){const s=e.command.args.match(/^(\S+)(?:\s+([\s\S]+))?$/u);s?.[1]?await runConsoleRequest(e.client,{input:"",sessionId:e.sessionId,...e.agentId?{agentId:e.agentId}:{},toolCall:{toolId:s[1],args:s[2]?JSON.parse(s[2]):{}}}):process.stdout.write("usage: /tool <tool-id> [json-args]\n")}(s):process.stdout.write(`unknown command: /${n.name}\n`)}return{done:!1,sessionId:s.sessionId,agentId:s.agentId}}({...s,command:t}):(await runConsoleRequest(s.client,await async function withSessionHistory(e,s){if(!s.sessionId||s.toolCall||s.workflow)return s;if("protocol"===e.mode)return{...s,metadata:{...s.metadata,consoleSession:!0,protocol:e.protocol}};const t=await async function buildSessionHistory(e,s){const t=(await e.requests(s)).filter(e=>"completed"===e.state).slice(-6),n=[];for(const s of t){const t=await e.inspectRequest(s.requestId),o=readHistoryInput(t),r=t?.output?.trim();o&&r&&n.push({role:"user",content:o},{role:"assistant",content:r})}return n.slice(-12)}(e,s.sessionId);return{...s,metadata:{...s.metadata,consoleSession:!0,openaiMessages:[...t,{role:"user",content:s.input}],openaiSessionHistory:t.length>0}}}(s.client,{input:s.line,sessionId:s.sessionId,...s.agentId?{agentId:s.agentId}:{}})),{done:!1,sessionId:s.sessionId,agentId:s.agentId}):{done:!1,sessionId:s.sessionId,agentId:s.agentId}}(s)}catch(e){return process.stdout.write(`error: ${function errorMessage(e){return e instanceof Error?e.message:String(e)}(e)}\n`),{done:!1,sessionId:s.sessionId,agentId:s.agentId}}}async function runConsoleRequest(s,t){const n=t.requestId??e(),o=s.stream(n,e=>process.stdout.write(`${e}\n`));try{const e=await s.request({...t,requestId:n});process.stdout.write(`${e.output}\n`)}finally{o.close()}}function createLocalConsoleClient(e,s){return{mode:"local",request:s=>e.request(s),health:async()=>"ok",sessions:async()=>e.listSessions(),requests:async s=>e.listRequests(s?{sessionId:s}:void 0),inspectRequest:async s=>e.inspectRequest(s),memories:async s=>e.listMemories(s?{namespace:s}:{}),clearSession:async s=>e.deleteSession(s).deletedCount,debug:async()=>e.inspect(),stream:(t,n)=>({close:e.subscribe(e=>{if(e.requestId===t){const t=c(e,s);t&&n(t)}})})}}function unsupportedList(e,s){return async()=>(process.stdout.write(`/${e} is not available through ${s}; use --protocol stable-runtime for operator features.\n`),[])}function unsupportedNumber(e,s){return async()=>(process.stdout.write(`/${e} is not available through ${s}; use --protocol stable-runtime for operator features.\n`),0)}function readHistoryInput(e){const s=Array.isArray(e?.metadata?.openaiMessages)?e.metadata.openaiMessages:[],t=s.map(e=>"object"==typeof e&&e?e:{}).filter(e=>"user"===e.role&&"string"==typeof e.content&&e.content.trim()).at(-1)?.content;return"string"==typeof t?t:e?.input.trim()||void 0}function switchSession(e,s){const t=s.trim()||e.sessionId;return process.stdout.write(`session ${t}\n`),{done:!1,sessionId:t,agentId:e.agentId}}function printJsonLines(e,s){if(0!==e.length)for(const t of e){const e="object"==typeof t&&t?t:{};process.stdout.write(`${String(e[s]??"")} ${JSON.stringify(t)}\n`)}else process.stdout.write("none\n")}async function getJson(e){const s=await fetch(e);if(!s.ok)throw new Error(`HTTP ${s.status}`);return await s.json()}
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import{createInterface as s}from"node:readline/promises";import{stdin as t,stdout as n}from"node:process";import{agentProtocolBaseUrl as o,daemonBaseUrl as r,isDaemonAvailable as i,sendAgentProtocolRuntimeRequest as a}from"../daemon/client.js";import{formatCliRuntimeEvent as c}from"../event-view.js";export async function runConsole(l){const u=await async function createConsoleClient(e){if("stable-runtime"!==e.args.clientProtocol){if("local"===e.args.runtimeMode)throw new Error(`stable-harness console: protocol ${e.args.clientProtocol} requires a protocol server; use --protocol stable-runtime for local mode`);return await async function createAgentProtocolConsoleClient(e,s){const t=e.clientProtocol,n=o(s,e.protocolUrl);if("stable-runtime"===t)throw new Error("stable-runtime is handled by the native console client");if(!(await fetch(`${n}/health`,{signal:AbortSignal.timeout(300)})).ok)throw new Error(`stable-harness console: protocol ${t} unavailable at ${n}`);return{mode:"protocol",protocol:t,baseUrl:n,request:e=>a(n,t,e),health:async()=>"ok",sessions:unsupportedList("sessions",t),requests:unsupportedList("requests",t),inspectRequest:async()=>{},memories:unsupportedList("memory",t),clearSession:unsupportedNumber("clear",t),debug:async()=>({protocol:t,baseUrl:n,note:"Protocol console exposes prompt turns only. Use --protocol stable-runtime for operator inspection."}),stream:()=>({close:()=>{}})}}(e.args,e.runtimePolicy)}if("local"===e.args.runtimeMode)return createLocalConsoleClient(await e.createRuntime(),e.eventView);const s=r(e.runtimePolicy,e.args.daemonUrl);if(await i(s,e.args.workspaceRoot))return function createDaemonConsoleClient(e,s){return{mode:"daemon",baseUrl:e,request:s=>async function postJson(e,s){const t=await fetch(e,{method:"POST",body:JSON.stringify(s)});if(!t.ok)throw new Error(`HTTP ${t.status}`);return await t.json()}(`${e}/requests`,s),health:async()=>(await getJson(`${e}/health`)).ok?"ok":"unhealthy",sessions:()=>getJson(`${e}/sessions`),requests:s=>getJson(`${e}/requests${s?`?sessionId=${encodeURIComponent(s)}`:""}`),inspectRequest:async s=>{const t=await fetch(`${e}/requests/${encodeURIComponent(s)}`);return t.ok?await t.json():void 0},memories:s=>getJson(`${e}/memories${s?`?namespace=${encodeURIComponent(s)}`:""}`),clearSession:async s=>(await async function deleteJson(e){const s=await fetch(e,{method:"DELETE"});if(!s.ok)throw new Error(`HTTP ${s.status}`);return await s.json()}(`${e}/sessions/${encodeURIComponent(s)}`)).deletedCount,debug:()=>getJson(`${e}/inspect`),stream:(t,n)=>function streamDaemonEvents(e,s,t,n){const o=new AbortController;return fetch(`${e}/events?requestId=${encodeURIComponent(s)}`,{signal:o.signal}).then(async e=>{const s=e.body?.getReader();if(!s)return;let o="";for(;;){const{value:e,done:r}=await s.read();if(r)return;o+=Buffer.from(e).toString("utf8");const i=o.split("\n\n");o=i.pop()??"";for(const e of i){const s=e.split("\n").find(e=>e.startsWith("data: "))?.slice(6);if(!s)continue;const o=c(JSON.parse(s),t);o&&n(o)}}}).catch(e=>{o.signal.aborted||process.stderr.write(`stable-harness console event stream failed: ${String(e)}\n`)}),{close:()=>o.abort()}}(e,t,s,n)}}(s,e.eventView);if("daemon"===e.args.runtimeMode)throw new Error(`stable-harness console: daemon required but unavailable at ${s}`);return createLocalConsoleClient(await e.createRuntime(),e.eventView)}(l);let d=l.args.sessionId??e(),m=l.args.agentId;process.stderr.write(`stable-harness console: ${function consoleRuntimeMessage(e,s){return"daemon"===e.mode?`connected to daemon at ${e.baseUrl}`:"protocol"===e.mode?`connected through ${e.protocol} at ${e.baseUrl}; prompt turns only, use --protocol stable-runtime for operator session APIs`:"local"===s?"local mode selected; running in-process":"no matching daemon found; running in-process"}(u,l.args.runtimeMode)}\n`),process.stdout.write(`session ${d}\n`);const p=s({input:t,output:n,terminal:process.stdout.isTTY});try{const e=process.stdout.isTTY?await async function runInteractiveLoop(e){let{sessionId:s,agentId:t}=e;for(;;){const n=await e.rl.question("stable> "),o=await handleConsoleLineSafely({client:e.client,line:n.trim(),sessionId:s,agentId:t,eventView:e.eventView});if(o.done)return o;s=o.sessionId,t=o.agentId}}({rl:p,client:u,sessionId:d,agentId:m,eventView:l.eventView}):await async function runPipedLoop(e){let{sessionId:s,agentId:t}=e;for await(const n of e.rl){const o=await handleConsoleLineSafely({client:e.client,line:n.trim(),sessionId:s,agentId:t,eventView:e.eventView});if(o.done)return o;s=o.sessionId,t=o.agentId}return{done:!0,sessionId:s,agentId:t}}({rl:p,client:u,sessionId:d,agentId:m,eventView:l.eventView});d=e.sessionId,m=e.agentId}catch(e){if(!function isEndOfInput(e){return e instanceof Error&&e.message.includes("closed")}(e))throw e}finally{p.close()}}async function handleConsoleLineSafely(s){try{return await async function handleConsoleLine(s){const t=function parseConsoleCommand(e){if(!e.startsWith("/"))return;const[s="",...t]=e.slice(1).trim().split(/\s+/u);return{name:s,args:t.join(" ").trim()}}(s.line);return s.line?t?await async function runConsoleCommand(s){const{client:t,command:n}=s;if("exit"===n.name||"quit"===n.name)return{...s,done:!0};if("help"===n.name)!function printHelp(){process.stdout.write(["/help Show console commands.","/session [id] Show or switch session.","/new Create and switch to a new session.","/sessions List known sessions.","/agent [id] Show or switch selected agent.","/tool <id> [json] Invoke a tool in the current session.","/requests [all] List current-session or all requests.","/memory [namespace] List memory records.","/clear Delete current session requests.","/health Check runtime health.","/protocol Show connection protocol and capability level.","/capabilities Show connection protocol and capability level.","/debug Print runtime inspection JSON.","/exit Leave console.",""].join("\n"))}();else if("health"===n.name)process.stdout.write(`${await t.health()}\n`);else if("debug"===n.name)process.stdout.write(`${JSON.stringify(await t.debug())}\n`);else if("protocol"===n.name||"capabilities"===n.name)!function printProtocolCapabilities(e){const s="protocol"===e.mode?e.protocol:"stable-runtime",t="protocol"===e.mode?"prompt-turns-only":"full-operator-session-api";process.stdout.write(`${JSON.stringify({mode:e.mode,protocol:s,baseUrl:e.baseUrl,session:t})}\n`)}(t);else{if("session"===n.name)return switchSession(s,n.args);if("new"===n.name)return switchSession(s,e());if("agent"===n.name)return function switchAgent(e,s){const t=s.trim()||void 0;return process.stdout.write(`agent ${t??"default"}\n`),{done:!1,sessionId:e.sessionId,agentId:t}}(s,n.args);"sessions"===n.name?printJsonLines(await t.sessions(),"sessionId"):"requests"===n.name?printJsonLines(await t.requests("all"===n.args?void 0:s.sessionId),"requestId"):"memory"===n.name||"memories"===n.name?printJsonLines(await t.memories(n.args||void 0),"id"):"clear"===n.name?process.stdout.write(`cleared ${await t.clearSession(s.sessionId)} requests from ${s.sessionId}\n`):"tool"===n.name?await async function runConsoleTool(e){const s=e.command.args.match(/^(\S+)(?:\s+([\s\S]+))?$/u);s?.[1]?await runConsoleRequest(e.client,{input:"",sessionId:e.sessionId,...e.agentId?{agentId:e.agentId}:{},toolCall:{toolId:s[1],args:s[2]?JSON.parse(s[2]):{}}}):process.stdout.write("usage: /tool <tool-id> [json-args]\n")}(s):process.stdout.write(`unknown command: /${n.name}\n`)}return{done:!1,sessionId:s.sessionId,agentId:s.agentId}}({...s,command:t}):(await runConsoleRequest(s.client,await async function withSessionHistory(e,s){if(!s.sessionId||s.toolCall||s.workflow)return s;if("protocol"===e.mode)return{...s,metadata:{...s.metadata,consoleSession:!0,protocol:e.protocol}};const t=await async function buildSessionHistory(e,s){const t=(await e.requests(s)).filter(e=>"completed"===e.state).slice(-6),n=[];for(const s of t){const t=await e.inspectRequest(s.requestId),o=readHistoryInput(t),r=t?.output?.trim();o&&r&&n.push({role:"user",content:o},{role:"assistant",content:r})}return n.slice(-12)}(e,s.sessionId);return{...s,metadata:{...s.metadata,consoleSession:!0,openaiMessages:[...t,{role:"user",content:s.input}],openaiSessionHistory:t.length>0}}}(s.client,{input:s.line,sessionId:s.sessionId,...s.agentId?{agentId:s.agentId}:{}})),{done:!1,sessionId:s.sessionId,agentId:s.agentId}):{done:!1,sessionId:s.sessionId,agentId:s.agentId}}(s)}catch(e){return process.stdout.write(`error: ${function errorMessage(e){return e instanceof Error?e.message:String(e)}(e)}\n`),{done:!1,sessionId:s.sessionId,agentId:s.agentId}}}async function runConsoleRequest(s,t){const n=t.requestId??e(),o=s.stream(n,e=>process.stdout.write(`${e}\n`));try{const e=await s.request({...t,requestId:n});process.stdout.write(`${e.output}\n`)}finally{o.close()}}function createLocalConsoleClient(e,s){return{mode:"local",request:s=>e.request(s),health:async()=>"ok",sessions:async()=>e.listSessions(),requests:async s=>e.listRequests(s?{sessionId:s}:void 0),inspectRequest:async s=>e.inspectRequest(s),memories:async s=>e.listMemories(s?{namespace:s}:{}),clearSession:async s=>e.deleteSession(s).deletedCount,debug:async()=>e.inspect(),stream:(t,n)=>({close:e.subscribe(e=>{if(e.requestId===t){const t=c(e,s);t&&n(t)}})})}}function unsupportedList(e,s){return async()=>(process.stdout.write(`/${e} is not available through ${s}; use --protocol stable-runtime for operator features.\n`),[])}function unsupportedNumber(e,s){return async()=>(process.stdout.write(`/${e} is not available through ${s}; use --protocol stable-runtime for operator features.\n`),0)}function readHistoryInput(e){const s=Array.isArray(e?.metadata?.openaiMessages)?e.metadata.openaiMessages:[],t=s.map(e=>"object"==typeof e&&e?e:{}).filter(e=>"user"===e.role&&"string"==typeof e.content&&e.content.trim()).at(-1)?.content;return"string"==typeof t?t:e?.input.trim()||void 0}function switchSession(e,s){const t=s.trim()||e.sessionId;return process.stdout.write(`session ${t}\n`),{done:!1,sessionId:t,agentId:e.agentId}}function printJsonLines(e,s){if(0!==e.length)for(const t of e){const e="object"==typeof t&&t?t:{};process.stdout.write(`${String(e[s]??"")} ${JSON.stringify(t)}\n`)}else process.stdout.write("none\n")}async function getJson(e){const s=await fetch(e);if(!s.ok)throw new Error(`HTTP ${s.status}`);return await s.json()}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"types": "dist/src/index.d.ts",
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"@langchain/langgraph-api": "^1.2.1",
|
|
17
|
-
"@stable-harness/adapter-deepagents": "0.0.
|
|
18
|
-
"@stable-harness/adapter-langgraph": "0.0.
|
|
19
|
-
"@stable-harness/core": "0.0.
|
|
20
|
-
"@stable-harness/memory": "0.0.
|
|
21
|
-
"@stable-harness/protocols": "0.0.
|
|
22
|
-
"@stable-harness/tool-gateway": "0.0.
|
|
23
|
-
"@stable-harness/workspace-yaml": "0.0.
|
|
17
|
+
"@stable-harness/adapter-deepagents": "0.0.107",
|
|
18
|
+
"@stable-harness/adapter-langgraph": "0.0.107",
|
|
19
|
+
"@stable-harness/core": "0.0.107",
|
|
20
|
+
"@stable-harness/memory": "0.0.107",
|
|
21
|
+
"@stable-harness/protocols": "0.0.107",
|
|
22
|
+
"@stable-harness/tool-gateway": "0.0.107",
|
|
23
|
+
"@stable-harness/workspace-yaml": "0.0.107"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
".": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@stable-harness/governance": "0.0.
|
|
15
|
-
"@stable-harness/memory": "0.0.
|
|
14
|
+
"@stable-harness/governance": "0.0.107",
|
|
15
|
+
"@stable-harness/memory": "0.0.107"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/evaluation",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
"main": "dist/src/index.js",
|
|
11
11
|
"types": "dist/src/index.d.ts",
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@stable-harness/core": "0.0.
|
|
13
|
+
"@stable-harness/core": "0.0.107"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -4,4 +4,23 @@ export type AgentProtocolServerOptions = {
|
|
|
4
4
|
baseUrl?: string;
|
|
5
5
|
enabledProtocols?: Array<"acp" | "a2a" | "agui">;
|
|
6
6
|
};
|
|
7
|
+
type JsonRpcMessage = {
|
|
8
|
+
jsonrpc?: string;
|
|
9
|
+
id?: string | number | null;
|
|
10
|
+
method?: string;
|
|
11
|
+
params?: unknown;
|
|
12
|
+
};
|
|
7
13
|
export declare function createAgentProtocolHttpServer(runtime: StableHarnessRuntime, options?: AgentProtocolServerOptions): import("node:http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
14
|
+
export declare function handleAcpJsonRpcMessage(runtime: StableHarnessRuntime, message: JsonRpcMessage): Promise<{
|
|
15
|
+
jsonrpc: string;
|
|
16
|
+
id: string | number | null | undefined;
|
|
17
|
+
result: unknown;
|
|
18
|
+
} | {
|
|
19
|
+
jsonrpc: string;
|
|
20
|
+
id: string | number | null | undefined;
|
|
21
|
+
error: {
|
|
22
|
+
code: number;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
} | undefined>;
|
|
26
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createServer as e}from"node:http";export function createAgentProtocolHttpServer(t,a={}){const r=new Set(a.enabledProtocols??["acp","a2a","agui"]);return e(async(e,s)=>{try{if("GET"===e.method&&"/health"===e.url)return void sendJson(s,200,{ok:!0,protocols:[...r]});if(r.has("a2a")&&await async function handleA2a(e,t,a,r){if("GET"===t.method&&("/.well-known/agent-card.json"===t.url||"/a2a/agent-card.json"===t.url))return sendJson(a,200,function createAgentCard(e,t){const a=e.inspect();return{protocolVersion:"1.0",name:a.workspaceRoot.split(/[\\/]/u).filter(Boolean).at(-1)??"stable-harness",description:"Stable Harness runtime agent endpoint",version:"0.0.0",url:t.baseUrl?`${t.baseUrl.replace(/\/$/u,"")}/a2a`:"/a2a",preferredTransport:"HTTP+JSON",capabilities:{streaming:!0},defaultInputModes:["text/plain"],defaultOutputModes:["text/plain"],skills:a.agents.map(e=>({id:e,name:e,description:e}))}}(e,r)),!0;const s=matchPath(t.url,/^\/a2a\/tasks\/([^/]+)$/u);if("GET"===t.method&&s){const t=e.inspectRequest(s);return sendJson(a,t?200:404,t?{task:toA2aTask(t)}:{error:"task_not_found"}),!0}if("GET"===t.method&&"/a2a/tasks"===t.url)return sendJson(a,200,{tasks:e.listRequests().map(toA2aTaskSummary)}),!0;const n=matchPath(t.url,/^\/a2a\/tasks\/([^/]+):cancel$/u);return"POST"===t.method&&n?(e.cancel(n,"a2a_cancel"),sendJson(a,200,{task:e.inspectRequest(n)}),!0):"POST"===t.method&&"/a2a/message:send"===t.url?(sendJson(a,200,{task:toA2aTask(await e.request(toA2aRuntimeRequest(await readJson(t))))}),!0):"POST"===t.method&&"/a2a/message:stream"===t.url?(await streamA2a(e,await readJson(t),a),!0):"POST"===t.method&&"/a2a/rpc"===t.url&&(await async function handleA2aRpc(e,t,a){if("SendMessage"===t.method){const r=await e.request(toA2aRuntimeRequest(readRecord(t.params)??{}));return void sendJson(a,200,jsonRpcResult(t.id,{task:toA2aTask(r)}))}if("SendStreamingMessage"!==t.method){if("GetTask"===t.method){const r=readString(readRecord(t.params)?.id),s=r?e.inspectRequest(r):void 0;return void sendJson(a,s?200:404,s?jsonRpcResult(t.id,toA2aTask(s)):jsonRpcError(t.id,-32004,"task_not_found"))}"ListTasks"!==t.method?sendJson(a,404,jsonRpcError(t.id,-32601,"method_not_found")):sendJson(a,200,jsonRpcResult(t.id,{tasks:e.listRequests().map(toA2aTaskSummary)}))}else await streamA2a(e,readRecord(t.params)??{},a,t.id)}(e,await readJson(t),a),!0)}(t,e,s,a))return;if(r.has("agui")&&await async function handleAgui(e,t,a){return"GET"===t.method&&"/ag-ui/capabilities"===t.url?(sendJson(a,200,{protocol:"ag-ui",transports:["http+sse"],events:["RunStarted","TextMessageChunk","Custom","RunFinished","RunError"]}),!0):"POST"===t.method&&"/ag-ui/runs"===t.url&&(await async function streamAgui(e,t,a){const r=readRuntimeRequestExtension(t),s=r?.requestId??readString(t.runId)??crypto.randomUUID(),n=r?.sessionId??readString(t.threadId)??readString(t.sessionId)??`thread-${s}`,o=readPromptText(t);writeSseHeaders(a),writeSse(a,{type:"RunStarted",threadId:n,runId:s,input:{messages:t.messages,input:o}});const d=e.subscribe(e=>{e.requestId===s&&writeSse(a,function toAguiEvent(e){return"runtime.progress.narration"===e.type?{type:"Custom",name:"stable-harness.progress",value:e}:"runtime.tool.direct.started"===e.type?{type:"ToolCallStart",toolCallId:`${e.requestId}:${e.toolId}`,toolCallName:e.toolId}:"runtime.tool.direct.completed"===e.type?{type:"ToolCallResult",messageId:`${e.requestId}-message`,toolCallId:`${e.requestId}:${e.toolId}`,content:e.output,role:"tool"}:{type:"Custom",name:`stable-harness.${e.type}`,value:e}}(e))});try{const d=await e.request(r??{input:o,requestId:s,sessionId:n,agentId:readString(t.agentId),metadata:{protocol:"ag-ui"}}),i=`${s}-message`;d.output&&writeSse(a,{type:"TextMessageChunk",messageId:i,role:"assistant",delta:d.output}),writeSse(a,{type:"RunFinished",threadId:n,runId:s,outcome:{type:"completed"===d.state?"success":"interrupt"},result:d.output})}catch(e){writeSse(a,{type:"RunError",threadId:n,runId:s,message:errorMessage(e)})}finally{d(),a.end()}}(e,await readJson(t),a),!0)}(t,e,s))return;if(r.has("acp")&&await async function handleAcp(e,t,a){if("GET"===t.method&&"/acp/capabilities"===t.url)return sendJson(a,200,{protocolVersion:1,agentCapabilities:{loadSession:!0,promptCapabilities:{embeddedContext:!0},_meta:{transport:"http-jsonrpc"}},agentInfo:{name:"stable-harness",title:"Stable Harness",version:"0.0.0"}}),!0;if("POST"===t.method&&"/acp"===t.url){const r=await readJson(t),s=await async function handleAcpMessage(e,t){if(!t.id&&"session/cancel"===t.method){const a=readString(readRecord(t.params)?.sessionId);return void(a&&e.listRequests({sessionId:a,state:"running"}).forEach(t=>e.cancel(t.requestId,"acp_cancel")))}if("initialize"===t.method)return jsonRpcResult(t.id,{protocolVersion:1,agentCapabilities:{loadSession:!0,promptCapabilities:{embeddedContext:!0},_meta:{transport:"http-jsonrpc"}},agentInfo:{name:"stable-harness",title:"Stable Harness",version:"0.0.0"}});if("session/new"===t.method)return jsonRpcResult(t.id,{sessionId:`acp-${crypto.randomUUID()}`});if("session/load"===t.method)return jsonRpcResult(t.id,{sessionId:readString(readRecord(t.params)?.sessionId)});if("session/list"===t.method)return jsonRpcResult(t.id,{sessions:e.listSessions()});if("session/prompt"===t.method){const a=readRecord(t.params)??{},r=readRuntimeRequestExtension(a),s=await e.request(r??{input:readPromptText(a),sessionId:readString(a.sessionId),agentId:readString(a.agentId),metadata:{protocol:"acp",transport:"http-jsonrpc"}});return jsonRpcResult(t.id,{stopReason:"completed"===s.state?"end_turn":"refusal",_meta:{requestId:s.requestId,output:s.output}})}return jsonRpcError(t.id,-32601,"method_not_found")}(e,r);return s?sendJson(a,200,s):a.writeHead(204).end(),!0}return!1}(t,e,s))return;sendJson(s,404,{error:"not_found"})}catch(e){sendJson(s,400,{error:errorMessage(e)})}})}async function streamA2a(e,t,a,r){const s=toA2aRuntimeRequest(t);writeSseHeaders(a);const n=e.subscribe(e=>{e.requestId===s.requestId&&writeSse(a,{jsonrpc:"2.0",id:r,result:{event:toA2aEvent(e)}})});try{const t=await e.request(s);writeSse(a,{jsonrpc:"2.0",id:r,result:{task:toA2aTask(t),final:!0}})}finally{n(),a.end()}}function toA2aRuntimeRequest(e){const t=readRuntimeRequestExtension(e);if(t)return{...t,metadata:{...t.metadata,protocol:"a2a"}};const a=readRecord(e.message)??e;return{input:readPromptText(a),requestId:readString(e.requestId)??readString(a.messageId),sessionId:readString(e.taskId)??readString(a.taskId)??readString(a.contextId),agentId:readString(e.agentId)??readString(e.metadata&&readRecord(e.metadata)?.agentId),metadata:{protocol:"a2a",configuration:e.configuration,metadata:e.metadata}}}function toA2aTask(e){const t="state"in e?e.state:e.summary.state,a="output"in e?e.output:e.output??"",r="requestId"in e?e.requestId:e.summary.requestId;return{id:r,contextId:"sessionId"in e?e.sessionId:e.summary.sessionId,status:{state:"completed"===t?"completed":t,message:a?{role:"agent",messageId:`${r}-response`,parts:[{kind:"text",text:a}]}:void 0}}}function toA2aTaskSummary(e){return{id:e.requestId,contextId:e.sessionId,status:{state:e.state},metadata:{agentId:e.agentId}}}function toA2aEvent(e){return{kind:"status-update",taskId:e.requestId,contextId:e.sessionId,status:{state:e.type},metadata:e}}function readRuntimeRequestExtension(e){const t=readRecord(e.runtimeRequest)??readRecord(readRecord(e.metadata)?.runtimeRequest);if(t)return{input:readString(t.input)??"",...readString(t.requestId)?{requestId:readString(t.requestId)}:{},...readString(t.sessionId)?{sessionId:readString(t.sessionId)}:{},...readString(t.agentId)?{agentId:readString(t.agentId)}:{},...readToolCall(t.toolCall)?{toolCall:readToolCall(t.toolCall)}:{},...readWorkflow(t.workflow)?{workflow:readWorkflow(t.workflow)}:{},...readRecord(t.metadata)?{metadata:readRecord(t.metadata)}:{}}}function readToolCall(e){const t=readRecord(e),a=readString(t?.toolId);return a?{toolId:a,args:t?.args}:void 0}function readWorkflow(e){const t=readRecord(e);if(t)return{...readString(t.workflowId)?{workflowId:readString(t.workflowId)}:{},...readString(t.routeId)?{routeId:readString(t.routeId)}:{},...void 0!==t.input?{input:t.input}:{},...readRecord(t.metadata)?{metadata:readRecord(t.metadata)}:{}}}function readPromptText(e){return"string"==typeof e.input?e.input:"string"==typeof e.text?e.text:"string"==typeof e.prompt?e.prompt:Array.isArray(e.parts)?e.parts.map(readPartText).filter(Boolean).join("\n"):Array.isArray(e.messages)?readPromptText(readRecord(e.messages.at(-1))??{}):Array.isArray(e.content)?e.content.map(readPartText).filter(Boolean).join("\n"):"string"==typeof e.content?e.content:""}function readPartText(e){const t=readRecord(e);return t?readString(t.text)??readString(t.content)??"":"string"==typeof e?e:""}function matchPath(e,t){const a=(e??"").match(t);return a?.[1]?decodeURIComponent(a[1]):void 0}function readRecord(e){return"object"!=typeof e||null===e||Array.isArray(e)?void 0:e}function readString(e){return"string"==typeof e&&e.trim()?e:void 0}function jsonRpcResult(e,t){return{jsonrpc:"2.0",id:e,result:t}}function jsonRpcError(e,t,a){return{jsonrpc:"2.0",id:e,error:{code:t,message:a}}}function sendJson(e,t,a){e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(a))}function writeSseHeaders(e){e.writeHead(200,{"content-type":"text/event-stream","cache-control":"no-cache",connection:"keep-alive"})}function writeSse(e,t){e.write(`data: ${JSON.stringify(t)}\n\n`)}async function readJson(e){const t=[];for await(const a of e)t.push(Buffer.isBuffer(a)?a:Buffer.from(a));return t.length>0?JSON.parse(Buffer.concat(t).toString("utf8")):{}}function errorMessage(e){return e instanceof Error?e.message:String(e)}
|
|
1
|
+
import{createServer as e}from"node:http";export function createAgentProtocolHttpServer(t,a={}){const r=new Set(a.enabledProtocols??["acp","a2a","agui"]);return e(async(e,s)=>{try{if("GET"===e.method&&"/health"===e.url)return void sendJson(s,200,{ok:!0,protocols:[...r]});if("GET"===e.method&&"/capabilities"===e.url)return void sendJson(s,200,function createProtocolCapabilityManifest(e){return{protocol:"stable-harness-agent-protocols",protocols:[...e].sort().map(e=>({id:e,transports:"acp"===e?["stdio","http-jsonrpc"]:"a2a"===e?["http-json","sse"]:["http-sse"],session:"acp"===e?{level:"protocol-session",mapsTo:"sessionId",operatorApi:!1}:"a2a"===e?{level:"continuity",mapsTo:"contextId|taskId",operatorApi:!1}:{level:"continuity",mapsTo:"threadId|sessionId",operatorApi:!1}})),stableRuntime:{session:{level:"operator-api",operatorApi:!0}}}}(r));if(r.has("a2a")&&await async function handleA2a(e,t,a,r){if("GET"===t.method&&("/.well-known/agent-card.json"===t.url||"/a2a/agent-card.json"===t.url))return sendJson(a,200,function createAgentCard(e,t){const a=e.inspect();return{protocolVersion:"1.0",name:a.workspaceRoot.split(/[\\/]/u).filter(Boolean).at(-1)??"stable-harness",description:"Stable Harness runtime agent endpoint",version:"0.0.0",url:t.baseUrl?`${t.baseUrl.replace(/\/$/u,"")}/a2a`:"/a2a",preferredTransport:"HTTP+JSON",capabilities:{streaming:!0},defaultInputModes:["text/plain"],defaultOutputModes:["text/plain"],skills:a.agents.map(e=>({id:e,name:e,description:e}))}}(e,r)),!0;const s=matchPath(t.url,/^\/a2a\/tasks\/([^/]+)$/u);if("GET"===t.method&&s){const t=e.inspectRequest(s);return sendJson(a,t?200:404,t?{task:toA2aTask(t)}:{error:"task_not_found"}),!0}if("GET"===t.method&&"/a2a/tasks"===t.url)return sendJson(a,200,{tasks:e.listRequests().map(toA2aTaskSummary)}),!0;const n=matchPath(t.url,/^\/a2a\/tasks\/([^/]+):cancel$/u);return"POST"===t.method&&n?(e.cancel(n,"a2a_cancel"),sendJson(a,200,{task:e.inspectRequest(n)}),!0):"POST"===t.method&&"/a2a/message:send"===t.url?(sendJson(a,200,{task:toA2aTask(await e.request(toA2aRuntimeRequest(await readJson(t))))}),!0):"POST"===t.method&&"/a2a/message:stream"===t.url?(await streamA2a(e,await readJson(t),a),!0):"POST"===t.method&&"/a2a/rpc"===t.url&&(await async function handleA2aRpc(e,t,a){if("SendMessage"===t.method){const r=await e.request(toA2aRuntimeRequest(readRecord(t.params)??{}));return void sendJson(a,200,jsonRpcResult(t.id,{task:toA2aTask(r)}))}if("SendStreamingMessage"!==t.method){if("GetTask"===t.method){const r=readString(readRecord(t.params)?.id),s=r?e.inspectRequest(r):void 0;return void sendJson(a,s?200:404,s?jsonRpcResult(t.id,toA2aTask(s)):jsonRpcError(t.id,-32004,"task_not_found"))}"ListTasks"!==t.method?sendJson(a,404,jsonRpcError(t.id,-32601,"method_not_found")):sendJson(a,200,jsonRpcResult(t.id,{tasks:e.listRequests().map(toA2aTaskSummary)}))}else await streamA2a(e,readRecord(t.params)??{},a,t.id)}(e,await readJson(t),a),!0)}(t,e,s,a))return;if(r.has("agui")&&await async function handleAgui(e,t,a){return"GET"===t.method&&"/ag-ui/capabilities"===t.url?(sendJson(a,200,{protocol:"ag-ui",transports:["http+sse"],events:["RunStarted","TextMessageChunk","Custom","RunFinished","RunError"]}),!0):"POST"===t.method&&"/ag-ui/runs"===t.url&&(await async function streamAgui(e,t,a){const r=readRuntimeRequestExtension(t),s=r?.requestId??readString(t.runId)??crypto.randomUUID(),n=r?.sessionId??readString(t.threadId)??readString(t.sessionId)??`thread-${s}`,o=readPromptText(t);writeSseHeaders(a),writeSse(a,{type:"RunStarted",threadId:n,runId:s,input:{messages:t.messages,input:o}});const d=e.subscribe(e=>{e.requestId===s&&writeSse(a,function toAguiEvent(e){return"runtime.progress.narration"===e.type?{type:"Custom",name:"stable-harness.progress",value:e}:"runtime.tool.direct.started"===e.type?{type:"ToolCallStart",toolCallId:`${e.requestId}:${e.toolId}`,toolCallName:e.toolId}:"runtime.tool.direct.completed"===e.type?{type:"ToolCallResult",messageId:`${e.requestId}-message`,toolCallId:`${e.requestId}:${e.toolId}`,content:e.output,role:"tool"}:{type:"Custom",name:`stable-harness.${e.type}`,value:e}}(e))});try{const d=await e.request(r??{input:o,requestId:s,sessionId:n,agentId:readString(t.agentId),metadata:{protocol:"ag-ui"}}),i=`${s}-message`;d.output&&writeSse(a,{type:"TextMessageChunk",messageId:i,role:"assistant",delta:d.output}),writeSse(a,{type:"RunFinished",threadId:n,runId:s,outcome:{type:"completed"===d.state?"success":"interrupt"},result:d.output})}catch(e){writeSse(a,{type:"RunError",threadId:n,runId:s,message:errorMessage(e)})}finally{d(),a.end()}}(e,await readJson(t),a),!0)}(t,e,s))return;if(r.has("acp")&&await async function handleAcp(e,t,a){if("GET"===t.method&&"/acp/capabilities"===t.url)return sendJson(a,200,{protocolVersion:1,agentCapabilities:{loadSession:!0,promptCapabilities:{embeddedContext:!0},_meta:{transport:"http-jsonrpc"}},agentInfo:{name:"stable-harness",title:"Stable Harness",version:"0.0.0"}}),!0;if("POST"===t.method&&"/acp"===t.url){const r=await readJson(t),s=await handleAcpJsonRpcMessage(e,r);return s?sendJson(a,200,s):a.writeHead(204).end(),!0}return!1}(t,e,s))return;sendJson(s,404,{error:"not_found"})}catch(e){sendJson(s,400,{error:errorMessage(e)})}})}export async function handleAcpJsonRpcMessage(e,t){if(!t.id&&"session/cancel"===t.method){const a=readString(readRecord(t.params)?.sessionId);return void(a&&e.listRequests({sessionId:a,state:"running"}).forEach(t=>e.cancel(t.requestId,"acp_cancel")))}if("initialize"===t.method)return jsonRpcResult(t.id,{protocolVersion:1,agentCapabilities:{loadSession:!0,promptCapabilities:{embeddedContext:!0},_meta:{transport:"http-jsonrpc"}},agentInfo:{name:"stable-harness",title:"Stable Harness",version:"0.0.0"}});if("session/new"===t.method)return jsonRpcResult(t.id,{sessionId:`acp-${crypto.randomUUID()}`});if("session/load"===t.method)return jsonRpcResult(t.id,{sessionId:readString(readRecord(t.params)?.sessionId)});if("session/list"===t.method)return jsonRpcResult(t.id,{sessions:e.listSessions()});if("session/prompt"===t.method){const a=readRecord(t.params)??{},r=readRuntimeRequestExtension(a),s=await e.request(r??{input:readPromptText(a),sessionId:readString(a.sessionId),agentId:readString(a.agentId),metadata:{protocol:"acp",transport:"http-jsonrpc"}});return jsonRpcResult(t.id,{stopReason:"completed"===s.state?"end_turn":"refusal",_meta:{requestId:s.requestId,output:s.output}})}return jsonRpcError(t.id,-32601,"method_not_found")}async function streamA2a(e,t,a,r){const s=toA2aRuntimeRequest(t);writeSseHeaders(a);const n=e.subscribe(e=>{e.requestId===s.requestId&&writeSse(a,{jsonrpc:"2.0",id:r,result:{event:toA2aEvent(e)}})});try{const t=await e.request(s);writeSse(a,{jsonrpc:"2.0",id:r,result:{task:toA2aTask(t),final:!0}})}finally{n(),a.end()}}function toA2aRuntimeRequest(e){const t=readRuntimeRequestExtension(e);if(t)return{...t,metadata:{...t.metadata,protocol:"a2a"}};const a=readRecord(e.message)??e;return{input:readPromptText(a),requestId:readString(e.requestId)??readString(a.messageId),sessionId:readString(e.taskId)??readString(a.taskId)??readString(a.contextId),agentId:readString(e.agentId)??readString(e.metadata&&readRecord(e.metadata)?.agentId),metadata:{protocol:"a2a",configuration:e.configuration,metadata:e.metadata}}}function toA2aTask(e){const t="state"in e?e.state:e.summary.state,a="output"in e?e.output:e.output??"",r="requestId"in e?e.requestId:e.summary.requestId;return{id:r,contextId:"sessionId"in e?e.sessionId:e.summary.sessionId,status:{state:"completed"===t?"completed":t,message:a?{role:"agent",messageId:`${r}-response`,parts:[{kind:"text",text:a}]}:void 0}}}function toA2aTaskSummary(e){return{id:e.requestId,contextId:e.sessionId,status:{state:e.state},metadata:{agentId:e.agentId}}}function toA2aEvent(e){const t="runtime.request.completed"===e.type?{role:"agent",messageId:`${e.requestId}-response`,parts:[{kind:"text",text:e.output}]}:void 0;return{kind:"status-update",taskId:e.requestId,contextId:e.sessionId,status:{state:(a=e.type,"runtime.request.started"===a?"working":"runtime.request.completed"===a?"completed":"runtime.request.failed"===a?"failed":"runtime.request.cancelled"===a?"canceled":"working"),message:t},metadata:{stableHarnessEvent:e}};var a}function readRuntimeRequestExtension(e){const t=readRecord(e.runtimeRequest)??readRecord(readRecord(e.metadata)?.runtimeRequest);if(t)return{input:readString(t.input)??"",...readString(t.requestId)?{requestId:readString(t.requestId)}:{},...readString(t.sessionId)?{sessionId:readString(t.sessionId)}:{},...readString(t.agentId)?{agentId:readString(t.agentId)}:{},...readToolCall(t.toolCall)?{toolCall:readToolCall(t.toolCall)}:{},...readWorkflow(t.workflow)?{workflow:readWorkflow(t.workflow)}:{},...readRecord(t.metadata)?{metadata:readRecord(t.metadata)}:{}}}function readToolCall(e){const t=readRecord(e),a=readString(t?.toolId);return a?{toolId:a,args:t?.args}:void 0}function readWorkflow(e){const t=readRecord(e);if(t)return{...readString(t.workflowId)?{workflowId:readString(t.workflowId)}:{},...readString(t.routeId)?{routeId:readString(t.routeId)}:{},...void 0!==t.input?{input:t.input}:{},...readRecord(t.metadata)?{metadata:readRecord(t.metadata)}:{}}}function readPromptText(e){return"string"==typeof e.input?e.input:"string"==typeof e.text?e.text:"string"==typeof e.prompt?e.prompt:Array.isArray(e.parts)?e.parts.map(readPartText).filter(Boolean).join("\n"):Array.isArray(e.messages)?readPromptText(readRecord(e.messages.at(-1))??{}):Array.isArray(e.content)?e.content.map(readPartText).filter(Boolean).join("\n"):"string"==typeof e.content?e.content:""}function readPartText(e){const t=readRecord(e);return t?readString(t.text)??readString(t.content)??"":"string"==typeof e?e:""}function matchPath(e,t){const a=(e??"").match(t);return a?.[1]?decodeURIComponent(a[1]):void 0}function readRecord(e){return"object"!=typeof e||null===e||Array.isArray(e)?void 0:e}function readString(e){return"string"==typeof e&&e.trim()?e:void 0}function jsonRpcResult(e,t){return{jsonrpc:"2.0",id:e,result:t}}function jsonRpcError(e,t,a){return{jsonrpc:"2.0",id:e,error:{code:t,message:a}}}function sendJson(e,t,a){e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(a))}function writeSseHeaders(e){e.writeHead(200,{"content-type":"text/event-stream","cache-control":"no-cache",connection:"keep-alive"})}function writeSse(e,t){e.write(`data: ${JSON.stringify(t)}\n\n`)}async function readJson(e){const t=[];for await(const a of e)t.push(Buffer.isBuffer(a)?a:Buffer.from(a));return t.length>0?JSON.parse(Buffer.concat(t).toString("utf8")):{}}function errorMessage(e){return e instanceof Error?e.message:String(e)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createInProcessClient } from "./in-process-client.js";
|
|
2
|
-
export { createAgentProtocolHttpServer } from "./agent-protocols.js";
|
|
2
|
+
export { createAgentProtocolHttpServer, handleAcpJsonRpcMessage } from "./agent-protocols.js";
|
|
3
3
|
export type { AgentProtocolServerOptions } from "./agent-protocols.js";
|
|
4
4
|
export { createHttpServer } from "./http-server.js";
|
|
5
5
|
export { createOpenAiCompatibleHttpServer } from "./openai-compatible.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{createInProcessClient}from"./in-process-client.js";export{createAgentProtocolHttpServer}from"./agent-protocols.js";export{createHttpServer}from"./http-server.js";export{createOpenAiCompatibleHttpServer}from"./openai-compatible.js";
|
|
1
|
+
export{createInProcessClient}from"./in-process-client.js";export{createAgentProtocolHttpServer,handleAcpJsonRpcMessage}from"./agent-protocols.js";export{createHttpServer}from"./http-server.js";export{createOpenAiCompatibleHttpServer}from"./openai-compatible.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/protocols",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
"main": "dist/src/index.js",
|
|
11
11
|
"types": "dist/src/index.d.ts",
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@stable-harness/core": "0.0.
|
|
13
|
+
"@stable-harness/core": "0.0.107"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stable-harness/workspace-yaml",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**/*.js",
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
".": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@stable-harness/core": "0.0.
|
|
14
|
+
"@stable-harness/core": "0.0.107"
|
|
15
15
|
}
|
|
16
16
|
}
|