@orangecheck/agent-openai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/index.d.mts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +138 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -0
- package/src/index.test.ts +91 -0
- package/src/index.ts +299 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OrangeCheck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @orangecheck/agent-openai
|
|
2
|
+
|
|
3
|
+
Wrap [OpenAI function calls](https://platform.openai.com/docs/guides/function-calling) with [OC Agent](https://github.com/orangecheck/oc-agent-protocol) scope enforcement and emit a signed `agent-action` envelope per invocation. Covers both the new **Responses API** (`function_call` items) and the legacy **Chat Completions** tool path (`tool_calls`).
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Function calling is the OpenAI-shaped way agents reach out to the world. Without scope enforcement and a cryptographic action receipt, "the model called this function" is the entire audit trail. With this adapter, every call carries: the agent address (BIP-322 signed), the active delegation, the exercised scope, the content hash of the inputs, and the OC Stamp anchor — replayable offline forever.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
**v0.0.1 · in design.** API shape is stable; canonicalization is locked. Production wiring (per-event streaming integration with the OpenAI SDK's `responses.stream()` / `chat.completions.create()`) lands when the first design partner names the integration.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @orangecheck/agent-openai
|
|
17
|
+
# peer deps:
|
|
18
|
+
npm i @orangecheck/agent-core @orangecheck/agent-signer
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { invokeWithStamp } from '@orangecheck/agent-openai';
|
|
25
|
+
|
|
26
|
+
// Pull a tool_call out of a Chat Completions response …
|
|
27
|
+
const toolCall = response.choices[0].message.tool_calls[0];
|
|
28
|
+
|
|
29
|
+
const { result, action, call } = await invokeWithStamp({
|
|
30
|
+
agent,
|
|
31
|
+
delegation,
|
|
32
|
+
call: toolCall, // raw OpenAI shape — we normalize
|
|
33
|
+
execute: async (c) => myInvoiceCreateImpl(c.arguments),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// `result` becomes the tool result you feed back to the model.
|
|
37
|
+
// `call` is the normalized OpenAiFunctionCall (id, name, arguments).
|
|
38
|
+
// `action` is the signed envelope; ship to your audit bundle / Nostr.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The same `invokeWithStamp` works on Responses-API `function_call` items — `parseFunctionCall` handles the `call_id` / `id` and string-vs-object `arguments` differences automatically.
|
|
42
|
+
|
|
43
|
+
## What gets stamped
|
|
44
|
+
|
|
45
|
+
The stamp's `content.hash` is a SHA-256 of the RFC-8785-canonicalized JSON:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"arguments": <canonical JSON of arguments>,
|
|
50
|
+
"id": "<call id>",
|
|
51
|
+
"name": "<function name>"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The action's `scope_exercised` defaults to `openai:function(name=<name>)`. Override via `scopeExercised`.
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
- `parseFunctionCall(raw)` — normalizes a Chat Completions `tool_call` or Responses `function_call` into `{id, name, arguments}`.
|
|
60
|
+
- `canonicalizeFunctionCall(call)` — RFC 8785 canonical bytes.
|
|
61
|
+
- `functionCallHash(call)` — `sha256:<64-hex>` for `content.hash`.
|
|
62
|
+
- `stampFunctionCall(input)` — sign without executing.
|
|
63
|
+
- `invokeWithStamp(input)` — sign + execute + return both.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT.
|
|
68
|
+
|
|
69
|
+
## Related
|
|
70
|
+
|
|
71
|
+
- [oc-agent-protocol](https://github.com/orangecheck/oc-agent-protocol) — normative wire format.
|
|
72
|
+
- [@orangecheck/agent-core](https://www.npmjs.com/package/@orangecheck/agent-core) — verifier library.
|
|
73
|
+
- [@orangecheck/agent-mcp](https://www.npmjs.com/package/@orangecheck/agent-mcp) — same pattern, for MCP.
|
|
74
|
+
- [@orangecheck/agent-anthropic](https://www.npmjs.com/package/@orangecheck/agent-anthropic) — same pattern, for Claude Tool Use.
|
|
75
|
+
- [console.ochk.io/integrations/openai](https://console.ochk.io/integrations/openai) — managed-tier story.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DelegationEnvelope } from '@orangecheck/agent-core';
|
|
2
|
+
import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
|
|
3
|
+
|
|
4
|
+
interface OpenAiFunctionCall {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
arguments: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
declare function parseFunctionCall(raw: unknown): OpenAiFunctionCall;
|
|
10
|
+
declare function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array;
|
|
11
|
+
declare function functionCallHash(c: OpenAiFunctionCall): string;
|
|
12
|
+
declare function functionCallActionId(agentAddress: string, signedAt: string, delegationId: string, scopeExercised: string, c: OpenAiFunctionCall): string;
|
|
13
|
+
interface StampFunctionCallInput {
|
|
14
|
+
agent: SignerRef;
|
|
15
|
+
delegation: DelegationEnvelope;
|
|
16
|
+
call: OpenAiFunctionCall | unknown;
|
|
17
|
+
scopeExercised?: string;
|
|
18
|
+
signedAt?: Date;
|
|
19
|
+
}
|
|
20
|
+
declare function stampFunctionCall(input: StampFunctionCallInput): Promise<ActionEnvelope>;
|
|
21
|
+
interface InvokeWithStampInput<T> extends StampFunctionCallInput {
|
|
22
|
+
execute: (call: OpenAiFunctionCall) => Promise<T>;
|
|
23
|
+
}
|
|
24
|
+
interface InvokeWithStampResult<T> {
|
|
25
|
+
result: T;
|
|
26
|
+
action: ActionEnvelope;
|
|
27
|
+
call: OpenAiFunctionCall;
|
|
28
|
+
}
|
|
29
|
+
declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
|
|
30
|
+
interface ConsoleClient {
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
apiToken: string;
|
|
33
|
+
projectId: string;
|
|
34
|
+
fetch?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
interface PostActionResult {
|
|
37
|
+
id: string;
|
|
38
|
+
project_id: string;
|
|
39
|
+
delegation_id: string;
|
|
40
|
+
}
|
|
41
|
+
declare function postActionToConsole(action: ActionEnvelope, client: ConsoleClient): Promise<PostActionResult>;
|
|
42
|
+
interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
|
|
43
|
+
console?: ConsoleClient;
|
|
44
|
+
}
|
|
45
|
+
interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
46
|
+
posted: PostActionResult | null;
|
|
47
|
+
}
|
|
48
|
+
declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
|
|
49
|
+
|
|
50
|
+
export { type ConsoleClient, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type OpenAiFunctionCall, type PostActionResult, type StampFunctionCallInput, canonicalizeFunctionCall, functionCallActionId, functionCallHash, invokeWithStamp, invokeWithStampAndPost, parseFunctionCall, postActionToConsole, stampFunctionCall };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DelegationEnvelope } from '@orangecheck/agent-core';
|
|
2
|
+
import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
|
|
3
|
+
|
|
4
|
+
interface OpenAiFunctionCall {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
arguments: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
declare function parseFunctionCall(raw: unknown): OpenAiFunctionCall;
|
|
10
|
+
declare function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array;
|
|
11
|
+
declare function functionCallHash(c: OpenAiFunctionCall): string;
|
|
12
|
+
declare function functionCallActionId(agentAddress: string, signedAt: string, delegationId: string, scopeExercised: string, c: OpenAiFunctionCall): string;
|
|
13
|
+
interface StampFunctionCallInput {
|
|
14
|
+
agent: SignerRef;
|
|
15
|
+
delegation: DelegationEnvelope;
|
|
16
|
+
call: OpenAiFunctionCall | unknown;
|
|
17
|
+
scopeExercised?: string;
|
|
18
|
+
signedAt?: Date;
|
|
19
|
+
}
|
|
20
|
+
declare function stampFunctionCall(input: StampFunctionCallInput): Promise<ActionEnvelope>;
|
|
21
|
+
interface InvokeWithStampInput<T> extends StampFunctionCallInput {
|
|
22
|
+
execute: (call: OpenAiFunctionCall) => Promise<T>;
|
|
23
|
+
}
|
|
24
|
+
interface InvokeWithStampResult<T> {
|
|
25
|
+
result: T;
|
|
26
|
+
action: ActionEnvelope;
|
|
27
|
+
call: OpenAiFunctionCall;
|
|
28
|
+
}
|
|
29
|
+
declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
|
|
30
|
+
interface ConsoleClient {
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
apiToken: string;
|
|
33
|
+
projectId: string;
|
|
34
|
+
fetch?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
interface PostActionResult {
|
|
37
|
+
id: string;
|
|
38
|
+
project_id: string;
|
|
39
|
+
delegation_id: string;
|
|
40
|
+
}
|
|
41
|
+
declare function postActionToConsole(action: ActionEnvelope, client: ConsoleClient): Promise<PostActionResult>;
|
|
42
|
+
interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
|
|
43
|
+
console?: ConsoleClient;
|
|
44
|
+
}
|
|
45
|
+
interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
46
|
+
posted: PostActionResult | null;
|
|
47
|
+
}
|
|
48
|
+
declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
|
|
49
|
+
|
|
50
|
+
export { type ConsoleClient, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type OpenAiFunctionCall, type PostActionResult, type StampFunctionCallInput, canonicalizeFunctionCall, functionCallActionId, functionCallHash, invokeWithStamp, invokeWithStampAndPost, parseFunctionCall, postActionToConsole, stampFunctionCall };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var sha256 = require('@noble/hashes/sha256');
|
|
4
|
+
var agentCore = require('@orangecheck/agent-core');
|
|
5
|
+
var agentSigner = require('@orangecheck/agent-signer');
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
function parseFunctionCall(raw) {
|
|
9
|
+
if (raw == null || typeof raw !== "object") {
|
|
10
|
+
throw new TypeError("parseFunctionCall: input is not an object");
|
|
11
|
+
}
|
|
12
|
+
const r = raw;
|
|
13
|
+
if (r.function && typeof r.function === "object") {
|
|
14
|
+
const f = r.function;
|
|
15
|
+
return {
|
|
16
|
+
id: String(r.id ?? ""),
|
|
17
|
+
name: String(f.name ?? ""),
|
|
18
|
+
arguments: parseArgs(f.arguments)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
id: String(r.id ?? r.call_id ?? ""),
|
|
23
|
+
name: String(r.name ?? ""),
|
|
24
|
+
arguments: parseArgs(r.arguments)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function parseArgs(args) {
|
|
28
|
+
if (args == null) return {};
|
|
29
|
+
if (typeof args === "object") return args;
|
|
30
|
+
if (typeof args === "string") {
|
|
31
|
+
try {
|
|
32
|
+
const v = JSON.parse(args);
|
|
33
|
+
if (v && typeof v === "object") return v;
|
|
34
|
+
return { _raw: args };
|
|
35
|
+
} catch {
|
|
36
|
+
return { _raw: args };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { _raw: String(args) };
|
|
40
|
+
}
|
|
41
|
+
function canonicalizeFunctionCall(c) {
|
|
42
|
+
const canon = {
|
|
43
|
+
arguments: c.arguments,
|
|
44
|
+
id: c.id,
|
|
45
|
+
name: c.name
|
|
46
|
+
};
|
|
47
|
+
const str = agentCore.canonicalize(canon);
|
|
48
|
+
return new TextEncoder().encode(str + "\n");
|
|
49
|
+
}
|
|
50
|
+
function functionCallHash(c) {
|
|
51
|
+
return "sha256:" + agentCore.hexEncode(sha256.sha256(canonicalizeFunctionCall(c)));
|
|
52
|
+
}
|
|
53
|
+
function functionCallActionId(agentAddress, signedAt, delegationId, scopeExercised, c) {
|
|
54
|
+
return agentCore.computeActionId({
|
|
55
|
+
address: agentAddress,
|
|
56
|
+
content_hash: functionCallHash(c),
|
|
57
|
+
content_length: canonicalizeFunctionCall(c).byteLength,
|
|
58
|
+
content_mime: "application/vnd.oc-agent.openai-function-call+json",
|
|
59
|
+
signed_at: signedAt,
|
|
60
|
+
delegation_id: delegationId,
|
|
61
|
+
scope_exercised: scopeExercised
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function stampFunctionCall(input) {
|
|
65
|
+
const fc = typeof input.call === "object" && input.call !== null && "arguments" in input.call && typeof input.call.arguments !== "string" ? input.call : parseFunctionCall(input.call);
|
|
66
|
+
const scopeExercised = input.scopeExercised ?? `openai:function(name=${fc.name})`;
|
|
67
|
+
const granted = (input.delegation.scopes ?? []).map(agentCore.parseScope);
|
|
68
|
+
const exercisedParsed = agentCore.parseScope(scopeExercised);
|
|
69
|
+
if (!granted.some((g) => agentCore.isSubScope(exercisedParsed, g))) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const bytes = canonicalizeFunctionCall(fc);
|
|
75
|
+
return agentSigner.signAsAgent({
|
|
76
|
+
agent: input.agent,
|
|
77
|
+
delegation: input.delegation,
|
|
78
|
+
content: { hash: functionCallHash(fc), length: bytes.byteLength },
|
|
79
|
+
mime: "application/vnd.oc-agent.openai-function-call+json",
|
|
80
|
+
scopeExercised,
|
|
81
|
+
signedAt: input.signedAt
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function invokeWithStamp(input) {
|
|
85
|
+
const action = await stampFunctionCall(input);
|
|
86
|
+
const fc = parseFunctionCall(input.call);
|
|
87
|
+
const result = await input.execute(fc);
|
|
88
|
+
return { result, action, call: fc };
|
|
89
|
+
}
|
|
90
|
+
async function postActionToConsole(action, client) {
|
|
91
|
+
const baseUrl = client.baseUrl ?? "https://console.ochk.io";
|
|
92
|
+
const f = client.fetch ?? fetch;
|
|
93
|
+
const body = {
|
|
94
|
+
project_id: client.projectId,
|
|
95
|
+
delegation_id: action.delegation_id,
|
|
96
|
+
agent_address: action.signer.address,
|
|
97
|
+
scope_exercised: action.scope_exercised,
|
|
98
|
+
content_hash: action.content.hash,
|
|
99
|
+
content_length: action.content.length,
|
|
100
|
+
content_mime: action.content.mime,
|
|
101
|
+
signed_at: action.signed_at,
|
|
102
|
+
signature: action.sig.value,
|
|
103
|
+
id: action.id
|
|
104
|
+
};
|
|
105
|
+
const r = await f(`${baseUrl}/api/actions`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
Authorization: `Bearer ${client.apiToken}`
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify(body)
|
|
112
|
+
});
|
|
113
|
+
if (!r.ok) {
|
|
114
|
+
let reason = `http_${r.status}`;
|
|
115
|
+
try {
|
|
116
|
+
const j2 = await r.json();
|
|
117
|
+
if (j2.reason) reason = j2.reason;
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`postActionToConsole failed: ${reason}`);
|
|
121
|
+
}
|
|
122
|
+
const j = await r.json();
|
|
123
|
+
return j.action;
|
|
124
|
+
}
|
|
125
|
+
async function invokeWithStampAndPost(input) {
|
|
126
|
+
const { result, action, call } = await invokeWithStamp(input);
|
|
127
|
+
let posted = null;
|
|
128
|
+
if (input.console) {
|
|
129
|
+
try {
|
|
130
|
+
posted = await postActionToConsole(action, input.console);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error("[oc-agent-openai] postActionToConsole failed:", err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { result, action, call, posted };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
exports.canonicalizeFunctionCall = canonicalizeFunctionCall;
|
|
139
|
+
exports.functionCallActionId = functionCallActionId;
|
|
140
|
+
exports.functionCallHash = functionCallHash;
|
|
141
|
+
exports.invokeWithStamp = invokeWithStamp;
|
|
142
|
+
exports.invokeWithStampAndPost = invokeWithStampAndPost;
|
|
143
|
+
exports.parseFunctionCall = parseFunctionCall;
|
|
144
|
+
exports.postActionToConsole = postActionToConsole;
|
|
145
|
+
exports.stampFunctionCall = stampFunctionCall;
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
147
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","parseScope","isSubScope","signAsAgent","j"],"mappings":"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","file":"index.js","sourcesContent":["// @orangecheck/agent-openai — wrap OpenAI function calls in OC Agent\n// action envelopes.\n//\n// Covers both surfaces:\n// - Responses API: response.output → tool_call items with\n// { type: 'function_call', name, arguments, call_id }\n// (`arguments` is a JSON string)\n// - Chat Completions: message.tool_calls[i] = { id, function: { name, arguments } }\n// (`arguments` is also a JSON string)\n//\n// Both shapes normalize through `parseFunctionCall()` to the canonical\n// {id, name, arguments} form before hashing.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n canonicalize,\n computeActionId,\n hexEncode,\n isSubScope,\n parseScope,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shape normalization\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OpenAiFunctionCall {\n /** OpenAI's `id` for this call (Chat Completions: `tool_call.id`; Responses: `call_id`). */\n id: string;\n /** The function name. */\n name: string;\n /** The (already-parsed) function arguments. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * Normalize either a Chat Completions `tool_call` or a Responses\n * `function_call` item into our canonical {id, name, arguments} shape.\n *\n * Accepts:\n * - { id, function: { name, arguments } } (Chat Completions)\n * - { call_id, name, arguments } (Responses, function_call)\n * - { id, name, arguments } (already normalized)\n *\n * `arguments` may be either a JSON string (OpenAI's wire shape) or an\n * already-parsed object (your post-processing). Both work.\n */\nexport function parseFunctionCall(raw: unknown): OpenAiFunctionCall {\n if (raw == null || typeof raw !== 'object') {\n throw new TypeError('parseFunctionCall: input is not an object');\n }\n const r = raw as Record<string, unknown>;\n\n // Chat Completions shape\n if (r.function && typeof r.function === 'object') {\n const f = r.function as Record<string, unknown>;\n return {\n id: String(r.id ?? ''),\n name: String(f.name ?? ''),\n arguments: parseArgs(f.arguments),\n };\n }\n\n // Responses API or already-normalized\n return {\n id: String(r.id ?? r.call_id ?? ''),\n name: String(r.name ?? ''),\n arguments: parseArgs(r.arguments),\n };\n}\n\nfunction parseArgs(args: unknown): Record<string, unknown> {\n if (args == null) return {};\n if (typeof args === 'object') return args as Record<string, unknown>;\n if (typeof args === 'string') {\n try {\n const v = JSON.parse(args);\n if (v && typeof v === 'object') return v as Record<string, unknown>;\n return { _raw: args };\n } catch {\n return { _raw: args };\n }\n }\n return { _raw: String(args) };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Canonicalization\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Produce the canonical bytes for an OpenAI function call. The bytes are\n * what the agent-action envelope's `content.hash` is a SHA-256 of.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"arguments\": <canonical JSON of arguments>,\n * \"id\": <call id>,\n * \"name\": <function name>\n * }\n */\nexport function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array {\n const canon = {\n arguments: c.arguments,\n id: c.id,\n name: c.name,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function functionCallHash(c: OpenAiFunctionCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeFunctionCall(c)));\n}\n\nexport function functionCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: OpenAiFunctionCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: functionCallHash(c),\n content_length: canonicalizeFunctionCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.openai-function-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping + invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampFunctionCallInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n /** Either a normalized `OpenAiFunctionCall` or a raw OpenAI tool_call / function_call object. */\n call: OpenAiFunctionCall | unknown;\n /** Defaults to `openai:function(name=<name>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampFunctionCall(\n input: StampFunctionCallInput\n): Promise<ActionEnvelope> {\n const fc =\n typeof input.call === 'object' &&\n input.call !== null &&\n 'arguments' in (input.call as Record<string, unknown>) &&\n typeof (input.call as Record<string, unknown>).arguments !== 'string'\n ? (input.call as OpenAiFunctionCall)\n : parseFunctionCall(input.call);\n\n const scopeExercised =\n input.scopeExercised ?? `openai:function(name=${fc.name})`;\n\n const granted = (input.delegation.scopes ?? []).map(parseScope);\n const exercisedParsed = parseScope(scopeExercised);\n if (!granted.some((g) => isSubScope(exercisedParsed, g))) {\n throw new Error(\n `stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeFunctionCall(fc);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: functionCallHash(fc), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.openai-function-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\nexport interface InvokeWithStampInput<T> extends StampFunctionCallInput {\n /** Your handler. Receives the normalized OpenAiFunctionCall. */\n execute: (call: OpenAiFunctionCall) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n call: OpenAiFunctionCall;\n}\n\nexport async function invokeWithStamp<T>(\n input: InvokeWithStampInput<T>\n): Promise<InvokeWithStampResult<T>> {\n const action = await stampFunctionCall(input);\n const fc = parseFunctionCall(input.call);\n const result = await input.execute(fc);\n return { result, action, call: fc };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Console integration: POST stamped actions to console.ochk.io/api/actions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ConsoleClient {\n /** Defaults to https://console.ochk.io. */\n baseUrl?: string;\n /** Bearer token from /settings § 03 (`ock_<hex>`). */\n apiToken: string;\n /** Project the action belongs to (proj_*). */\n projectId: string;\n /** Optional fetch override for runtimes that need it. */\n fetch?: typeof fetch;\n}\n\nexport interface PostActionResult {\n id: string;\n project_id: string;\n delegation_id: string;\n}\n\n/**\n * POST a stamped action envelope to console.ochk.io/api/actions. The\n * console re-derives the action id, validates agent-must-match-\n * delegation, persists, fans out to Nostr (kind 30084), submits to\n * OC Stamp, and triggers any subscribed webhooks. Throws on non-2xx\n * with the server's reason string.\n */\nexport async function postActionToConsole(\n action: ActionEnvelope,\n client: ConsoleClient\n): Promise<PostActionResult> {\n const baseUrl = client.baseUrl ?? 'https://console.ochk.io';\n const f = client.fetch ?? fetch;\n const body = {\n project_id: client.projectId,\n delegation_id: action.delegation_id,\n agent_address: action.signer.address,\n scope_exercised: action.scope_exercised,\n content_hash: action.content.hash,\n content_length: action.content.length,\n content_mime: action.content.mime,\n signed_at: action.signed_at,\n signature: action.sig.value,\n id: action.id,\n };\n const r = await f(`${baseUrl}/api/actions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${client.apiToken}`,\n },\n body: JSON.stringify(body),\n });\n if (!r.ok) {\n let reason = `http_${r.status}`;\n try {\n const j = (await r.json()) as { reason?: string };\n if (j.reason) reason = j.reason;\n } catch {\n // body wasn't json\n }\n throw new Error(`postActionToConsole failed: ${reason}`);\n }\n const j = (await r.json()) as { ok: true; action: PostActionResult };\n return j.action;\n}\n\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Console credentials. If absent, behaves like invokeWithStamp. */\n console?: ConsoleClient;\n}\n\nexport interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {\n /** Set when console is configured AND the POST succeeded. */\n posted: PostActionResult | null;\n}\n\nexport async function invokeWithStampAndPost<T>(\n input: InvokeWithStampAndPostInput<T>\n): Promise<InvokeWithStampAndPostResult<T>> {\n const { result, action, call } = await invokeWithStamp(input);\n let posted: PostActionResult | null = null;\n if (input.console) {\n try {\n posted = await postActionToConsole(action, input.console);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[oc-agent-openai] postActionToConsole failed:', err);\n }\n }\n return { result, action, call, posted };\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
+
import { canonicalize, hexEncode, computeActionId, parseScope, isSubScope } from '@orangecheck/agent-core';
|
|
3
|
+
import { signAsAgent } from '@orangecheck/agent-signer';
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
function parseFunctionCall(raw) {
|
|
7
|
+
if (raw == null || typeof raw !== "object") {
|
|
8
|
+
throw new TypeError("parseFunctionCall: input is not an object");
|
|
9
|
+
}
|
|
10
|
+
const r = raw;
|
|
11
|
+
if (r.function && typeof r.function === "object") {
|
|
12
|
+
const f = r.function;
|
|
13
|
+
return {
|
|
14
|
+
id: String(r.id ?? ""),
|
|
15
|
+
name: String(f.name ?? ""),
|
|
16
|
+
arguments: parseArgs(f.arguments)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
id: String(r.id ?? r.call_id ?? ""),
|
|
21
|
+
name: String(r.name ?? ""),
|
|
22
|
+
arguments: parseArgs(r.arguments)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function parseArgs(args) {
|
|
26
|
+
if (args == null) return {};
|
|
27
|
+
if (typeof args === "object") return args;
|
|
28
|
+
if (typeof args === "string") {
|
|
29
|
+
try {
|
|
30
|
+
const v = JSON.parse(args);
|
|
31
|
+
if (v && typeof v === "object") return v;
|
|
32
|
+
return { _raw: args };
|
|
33
|
+
} catch {
|
|
34
|
+
return { _raw: args };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { _raw: String(args) };
|
|
38
|
+
}
|
|
39
|
+
function canonicalizeFunctionCall(c) {
|
|
40
|
+
const canon = {
|
|
41
|
+
arguments: c.arguments,
|
|
42
|
+
id: c.id,
|
|
43
|
+
name: c.name
|
|
44
|
+
};
|
|
45
|
+
const str = canonicalize(canon);
|
|
46
|
+
return new TextEncoder().encode(str + "\n");
|
|
47
|
+
}
|
|
48
|
+
function functionCallHash(c) {
|
|
49
|
+
return "sha256:" + hexEncode(sha256(canonicalizeFunctionCall(c)));
|
|
50
|
+
}
|
|
51
|
+
function functionCallActionId(agentAddress, signedAt, delegationId, scopeExercised, c) {
|
|
52
|
+
return computeActionId({
|
|
53
|
+
address: agentAddress,
|
|
54
|
+
content_hash: functionCallHash(c),
|
|
55
|
+
content_length: canonicalizeFunctionCall(c).byteLength,
|
|
56
|
+
content_mime: "application/vnd.oc-agent.openai-function-call+json",
|
|
57
|
+
signed_at: signedAt,
|
|
58
|
+
delegation_id: delegationId,
|
|
59
|
+
scope_exercised: scopeExercised
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function stampFunctionCall(input) {
|
|
63
|
+
const fc = typeof input.call === "object" && input.call !== null && "arguments" in input.call && typeof input.call.arguments !== "string" ? input.call : parseFunctionCall(input.call);
|
|
64
|
+
const scopeExercised = input.scopeExercised ?? `openai:function(name=${fc.name})`;
|
|
65
|
+
const granted = (input.delegation.scopes ?? []).map(parseScope);
|
|
66
|
+
const exercisedParsed = parseScope(scopeExercised);
|
|
67
|
+
if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const bytes = canonicalizeFunctionCall(fc);
|
|
73
|
+
return signAsAgent({
|
|
74
|
+
agent: input.agent,
|
|
75
|
+
delegation: input.delegation,
|
|
76
|
+
content: { hash: functionCallHash(fc), length: bytes.byteLength },
|
|
77
|
+
mime: "application/vnd.oc-agent.openai-function-call+json",
|
|
78
|
+
scopeExercised,
|
|
79
|
+
signedAt: input.signedAt
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function invokeWithStamp(input) {
|
|
83
|
+
const action = await stampFunctionCall(input);
|
|
84
|
+
const fc = parseFunctionCall(input.call);
|
|
85
|
+
const result = await input.execute(fc);
|
|
86
|
+
return { result, action, call: fc };
|
|
87
|
+
}
|
|
88
|
+
async function postActionToConsole(action, client) {
|
|
89
|
+
const baseUrl = client.baseUrl ?? "https://console.ochk.io";
|
|
90
|
+
const f = client.fetch ?? fetch;
|
|
91
|
+
const body = {
|
|
92
|
+
project_id: client.projectId,
|
|
93
|
+
delegation_id: action.delegation_id,
|
|
94
|
+
agent_address: action.signer.address,
|
|
95
|
+
scope_exercised: action.scope_exercised,
|
|
96
|
+
content_hash: action.content.hash,
|
|
97
|
+
content_length: action.content.length,
|
|
98
|
+
content_mime: action.content.mime,
|
|
99
|
+
signed_at: action.signed_at,
|
|
100
|
+
signature: action.sig.value,
|
|
101
|
+
id: action.id
|
|
102
|
+
};
|
|
103
|
+
const r = await f(`${baseUrl}/api/actions`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
Authorization: `Bearer ${client.apiToken}`
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify(body)
|
|
110
|
+
});
|
|
111
|
+
if (!r.ok) {
|
|
112
|
+
let reason = `http_${r.status}`;
|
|
113
|
+
try {
|
|
114
|
+
const j2 = await r.json();
|
|
115
|
+
if (j2.reason) reason = j2.reason;
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`postActionToConsole failed: ${reason}`);
|
|
119
|
+
}
|
|
120
|
+
const j = await r.json();
|
|
121
|
+
return j.action;
|
|
122
|
+
}
|
|
123
|
+
async function invokeWithStampAndPost(input) {
|
|
124
|
+
const { result, action, call } = await invokeWithStamp(input);
|
|
125
|
+
let posted = null;
|
|
126
|
+
if (input.console) {
|
|
127
|
+
try {
|
|
128
|
+
posted = await postActionToConsole(action, input.console);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error("[oc-agent-openai] postActionToConsole failed:", err);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { result, action, call, posted };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { canonicalizeFunctionCall, functionCallActionId, functionCallHash, invokeWithStamp, invokeWithStampAndPost, parseFunctionCall, postActionToConsole, stampFunctionCall };
|
|
137
|
+
//# sourceMappingURL=index.mjs.map
|
|
138
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["j"],"mappings":"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","file":"index.mjs","sourcesContent":["// @orangecheck/agent-openai — wrap OpenAI function calls in OC Agent\n// action envelopes.\n//\n// Covers both surfaces:\n// - Responses API: response.output → tool_call items with\n// { type: 'function_call', name, arguments, call_id }\n// (`arguments` is a JSON string)\n// - Chat Completions: message.tool_calls[i] = { id, function: { name, arguments } }\n// (`arguments` is also a JSON string)\n//\n// Both shapes normalize through `parseFunctionCall()` to the canonical\n// {id, name, arguments} form before hashing.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n canonicalize,\n computeActionId,\n hexEncode,\n isSubScope,\n parseScope,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shape normalization\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OpenAiFunctionCall {\n /** OpenAI's `id` for this call (Chat Completions: `tool_call.id`; Responses: `call_id`). */\n id: string;\n /** The function name. */\n name: string;\n /** The (already-parsed) function arguments. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * Normalize either a Chat Completions `tool_call` or a Responses\n * `function_call` item into our canonical {id, name, arguments} shape.\n *\n * Accepts:\n * - { id, function: { name, arguments } } (Chat Completions)\n * - { call_id, name, arguments } (Responses, function_call)\n * - { id, name, arguments } (already normalized)\n *\n * `arguments` may be either a JSON string (OpenAI's wire shape) or an\n * already-parsed object (your post-processing). Both work.\n */\nexport function parseFunctionCall(raw: unknown): OpenAiFunctionCall {\n if (raw == null || typeof raw !== 'object') {\n throw new TypeError('parseFunctionCall: input is not an object');\n }\n const r = raw as Record<string, unknown>;\n\n // Chat Completions shape\n if (r.function && typeof r.function === 'object') {\n const f = r.function as Record<string, unknown>;\n return {\n id: String(r.id ?? ''),\n name: String(f.name ?? ''),\n arguments: parseArgs(f.arguments),\n };\n }\n\n // Responses API or already-normalized\n return {\n id: String(r.id ?? r.call_id ?? ''),\n name: String(r.name ?? ''),\n arguments: parseArgs(r.arguments),\n };\n}\n\nfunction parseArgs(args: unknown): Record<string, unknown> {\n if (args == null) return {};\n if (typeof args === 'object') return args as Record<string, unknown>;\n if (typeof args === 'string') {\n try {\n const v = JSON.parse(args);\n if (v && typeof v === 'object') return v as Record<string, unknown>;\n return { _raw: args };\n } catch {\n return { _raw: args };\n }\n }\n return { _raw: String(args) };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Canonicalization\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Produce the canonical bytes for an OpenAI function call. The bytes are\n * what the agent-action envelope's `content.hash` is a SHA-256 of.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"arguments\": <canonical JSON of arguments>,\n * \"id\": <call id>,\n * \"name\": <function name>\n * }\n */\nexport function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array {\n const canon = {\n arguments: c.arguments,\n id: c.id,\n name: c.name,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function functionCallHash(c: OpenAiFunctionCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeFunctionCall(c)));\n}\n\nexport function functionCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: OpenAiFunctionCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: functionCallHash(c),\n content_length: canonicalizeFunctionCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.openai-function-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping + invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampFunctionCallInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n /** Either a normalized `OpenAiFunctionCall` or a raw OpenAI tool_call / function_call object. */\n call: OpenAiFunctionCall | unknown;\n /** Defaults to `openai:function(name=<name>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampFunctionCall(\n input: StampFunctionCallInput\n): Promise<ActionEnvelope> {\n const fc =\n typeof input.call === 'object' &&\n input.call !== null &&\n 'arguments' in (input.call as Record<string, unknown>) &&\n typeof (input.call as Record<string, unknown>).arguments !== 'string'\n ? (input.call as OpenAiFunctionCall)\n : parseFunctionCall(input.call);\n\n const scopeExercised =\n input.scopeExercised ?? `openai:function(name=${fc.name})`;\n\n const granted = (input.delegation.scopes ?? []).map(parseScope);\n const exercisedParsed = parseScope(scopeExercised);\n if (!granted.some((g) => isSubScope(exercisedParsed, g))) {\n throw new Error(\n `stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeFunctionCall(fc);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: functionCallHash(fc), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.openai-function-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\nexport interface InvokeWithStampInput<T> extends StampFunctionCallInput {\n /** Your handler. Receives the normalized OpenAiFunctionCall. */\n execute: (call: OpenAiFunctionCall) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n call: OpenAiFunctionCall;\n}\n\nexport async function invokeWithStamp<T>(\n input: InvokeWithStampInput<T>\n): Promise<InvokeWithStampResult<T>> {\n const action = await stampFunctionCall(input);\n const fc = parseFunctionCall(input.call);\n const result = await input.execute(fc);\n return { result, action, call: fc };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Console integration: POST stamped actions to console.ochk.io/api/actions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ConsoleClient {\n /** Defaults to https://console.ochk.io. */\n baseUrl?: string;\n /** Bearer token from /settings § 03 (`ock_<hex>`). */\n apiToken: string;\n /** Project the action belongs to (proj_*). */\n projectId: string;\n /** Optional fetch override for runtimes that need it. */\n fetch?: typeof fetch;\n}\n\nexport interface PostActionResult {\n id: string;\n project_id: string;\n delegation_id: string;\n}\n\n/**\n * POST a stamped action envelope to console.ochk.io/api/actions. The\n * console re-derives the action id, validates agent-must-match-\n * delegation, persists, fans out to Nostr (kind 30084), submits to\n * OC Stamp, and triggers any subscribed webhooks. Throws on non-2xx\n * with the server's reason string.\n */\nexport async function postActionToConsole(\n action: ActionEnvelope,\n client: ConsoleClient\n): Promise<PostActionResult> {\n const baseUrl = client.baseUrl ?? 'https://console.ochk.io';\n const f = client.fetch ?? fetch;\n const body = {\n project_id: client.projectId,\n delegation_id: action.delegation_id,\n agent_address: action.signer.address,\n scope_exercised: action.scope_exercised,\n content_hash: action.content.hash,\n content_length: action.content.length,\n content_mime: action.content.mime,\n signed_at: action.signed_at,\n signature: action.sig.value,\n id: action.id,\n };\n const r = await f(`${baseUrl}/api/actions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${client.apiToken}`,\n },\n body: JSON.stringify(body),\n });\n if (!r.ok) {\n let reason = `http_${r.status}`;\n try {\n const j = (await r.json()) as { reason?: string };\n if (j.reason) reason = j.reason;\n } catch {\n // body wasn't json\n }\n throw new Error(`postActionToConsole failed: ${reason}`);\n }\n const j = (await r.json()) as { ok: true; action: PostActionResult };\n return j.action;\n}\n\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Console credentials. If absent, behaves like invokeWithStamp. */\n console?: ConsoleClient;\n}\n\nexport interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {\n /** Set when console is configured AND the POST succeeded. */\n posted: PostActionResult | null;\n}\n\nexport async function invokeWithStampAndPost<T>(\n input: InvokeWithStampAndPostInput<T>\n): Promise<InvokeWithStampAndPostResult<T>> {\n const { result, action, call } = await invokeWithStamp(input);\n let posted: PostActionResult | null = null;\n if (input.console) {\n try {\n posted = await postActionToConsole(action, input.console);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[oc-agent-openai] postActionToConsole failed:', err);\n }\n }\n return { result, action, call, posted };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orangecheck/agent-openai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Wrap OpenAI function-calling (Responses API + Chat Completions tool path) with OC Agent scope enforcement and emit a signed agent-action envelope per function invocation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bitcoin",
|
|
7
|
+
"agent",
|
|
8
|
+
"authority",
|
|
9
|
+
"openai",
|
|
10
|
+
"responses",
|
|
11
|
+
"function-calling",
|
|
12
|
+
"tool-use",
|
|
13
|
+
"bip322",
|
|
14
|
+
"oc-agent",
|
|
15
|
+
"orangecheck"
|
|
16
|
+
],
|
|
17
|
+
"author": "OrangeCheck",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/orangecheck/oc-packages.git",
|
|
22
|
+
"directory": "agent-openai"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/orangecheck/oc-agent-protocol",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/orangecheck/oc-agent-protocol/issues"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"module": "./dist/index.mjs",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.mjs",
|
|
35
|
+
"require": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"src",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"dev": "tsup --watch",
|
|
47
|
+
"type-check": "tsc --noEmit",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"clean": "rm -rf dist",
|
|
51
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@noble/hashes": "^1.5.0",
|
|
55
|
+
"@orangecheck/agent-core": "^0.3.0",
|
|
56
|
+
"@orangecheck/agent-signer": "^0.1.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^22.10.2",
|
|
60
|
+
"tsup": "^8.3.5",
|
|
61
|
+
"typescript": "^5.7.2",
|
|
62
|
+
"vitest": "^3.2.4"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
canonicalizeFunctionCall,
|
|
5
|
+
functionCallHash,
|
|
6
|
+
parseFunctionCall,
|
|
7
|
+
} from './index.js';
|
|
8
|
+
|
|
9
|
+
describe('parseFunctionCall', () => {
|
|
10
|
+
it('parses a Chat Completions tool_call', () => {
|
|
11
|
+
const c = parseFunctionCall({
|
|
12
|
+
id: 'call_abc',
|
|
13
|
+
type: 'function',
|
|
14
|
+
function: {
|
|
15
|
+
name: 'invoice.create',
|
|
16
|
+
arguments: '{"customer":"acme","amount":14.2}',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
expect(c.id).toBe('call_abc');
|
|
20
|
+
expect(c.name).toBe('invoice.create');
|
|
21
|
+
expect(c.arguments).toEqual({ customer: 'acme', amount: 14.2 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('parses a Responses function_call item', () => {
|
|
25
|
+
const c = parseFunctionCall({
|
|
26
|
+
type: 'function_call',
|
|
27
|
+
call_id: 'call_xyz',
|
|
28
|
+
name: 'invoice.refund',
|
|
29
|
+
arguments: '{"order":"o_99","amount":22.5}',
|
|
30
|
+
});
|
|
31
|
+
expect(c.id).toBe('call_xyz');
|
|
32
|
+
expect(c.name).toBe('invoice.refund');
|
|
33
|
+
expect(c.arguments).toEqual({ order: 'o_99', amount: 22.5 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('handles already-parsed arguments', () => {
|
|
37
|
+
const c = parseFunctionCall({
|
|
38
|
+
id: 'call_abc',
|
|
39
|
+
name: 'invoice.create',
|
|
40
|
+
arguments: { customer: 'acme', amount: 14.2 },
|
|
41
|
+
});
|
|
42
|
+
expect(c.arguments).toEqual({ customer: 'acme', amount: 14.2 });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('handles malformed argument strings without throwing', () => {
|
|
46
|
+
const c = parseFunctionCall({
|
|
47
|
+
id: 'call_abc',
|
|
48
|
+
name: 'invoice.create',
|
|
49
|
+
arguments: 'not-json',
|
|
50
|
+
});
|
|
51
|
+
expect(c.arguments).toEqual({ _raw: 'not-json' });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('canonicalizeFunctionCall', () => {
|
|
56
|
+
it('produces byte-identical output across Chat Completions vs Responses shapes', () => {
|
|
57
|
+
const cc = parseFunctionCall({
|
|
58
|
+
id: 'call_abc',
|
|
59
|
+
type: 'function',
|
|
60
|
+
function: {
|
|
61
|
+
name: 'invoice.create',
|
|
62
|
+
arguments: '{"customer":"acme","amount":14.2}',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const rsp = parseFunctionCall({
|
|
66
|
+
type: 'function_call',
|
|
67
|
+
call_id: 'call_abc',
|
|
68
|
+
name: 'invoice.create',
|
|
69
|
+
arguments: '{"amount":14.2,"customer":"acme"}',
|
|
70
|
+
});
|
|
71
|
+
expect(canonicalizeFunctionCall(cc)).toEqual(canonicalizeFunctionCall(rsp));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('differs when arguments differ', () => {
|
|
75
|
+
const a = functionCallHash(
|
|
76
|
+
parseFunctionCall({
|
|
77
|
+
id: 'c',
|
|
78
|
+
name: 'x',
|
|
79
|
+
arguments: '{"a":1}',
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
const b = functionCallHash(
|
|
83
|
+
parseFunctionCall({
|
|
84
|
+
id: 'c',
|
|
85
|
+
name: 'x',
|
|
86
|
+
arguments: '{"a":2}',
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
expect(a).not.toBe(b);
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// @orangecheck/agent-openai — wrap OpenAI function calls in OC Agent
|
|
2
|
+
// action envelopes.
|
|
3
|
+
//
|
|
4
|
+
// Covers both surfaces:
|
|
5
|
+
// - Responses API: response.output → tool_call items with
|
|
6
|
+
// { type: 'function_call', name, arguments, call_id }
|
|
7
|
+
// (`arguments` is a JSON string)
|
|
8
|
+
// - Chat Completions: message.tool_calls[i] = { id, function: { name, arguments } }
|
|
9
|
+
// (`arguments` is also a JSON string)
|
|
10
|
+
//
|
|
11
|
+
// Both shapes normalize through `parseFunctionCall()` to the canonical
|
|
12
|
+
// {id, name, arguments} form before hashing.
|
|
13
|
+
|
|
14
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
15
|
+
import {
|
|
16
|
+
canonicalize,
|
|
17
|
+
computeActionId,
|
|
18
|
+
hexEncode,
|
|
19
|
+
isSubScope,
|
|
20
|
+
parseScope,
|
|
21
|
+
type DelegationEnvelope,
|
|
22
|
+
} from '@orangecheck/agent-core';
|
|
23
|
+
import {
|
|
24
|
+
signAsAgent,
|
|
25
|
+
type ActionEnvelope,
|
|
26
|
+
type SignerRef,
|
|
27
|
+
} from '@orangecheck/agent-signer';
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Shape normalization
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface OpenAiFunctionCall {
|
|
34
|
+
/** OpenAI's `id` for this call (Chat Completions: `tool_call.id`; Responses: `call_id`). */
|
|
35
|
+
id: string;
|
|
36
|
+
/** The function name. */
|
|
37
|
+
name: string;
|
|
38
|
+
/** The (already-parsed) function arguments. */
|
|
39
|
+
arguments: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize either a Chat Completions `tool_call` or a Responses
|
|
44
|
+
* `function_call` item into our canonical {id, name, arguments} shape.
|
|
45
|
+
*
|
|
46
|
+
* Accepts:
|
|
47
|
+
* - { id, function: { name, arguments } } (Chat Completions)
|
|
48
|
+
* - { call_id, name, arguments } (Responses, function_call)
|
|
49
|
+
* - { id, name, arguments } (already normalized)
|
|
50
|
+
*
|
|
51
|
+
* `arguments` may be either a JSON string (OpenAI's wire shape) or an
|
|
52
|
+
* already-parsed object (your post-processing). Both work.
|
|
53
|
+
*/
|
|
54
|
+
export function parseFunctionCall(raw: unknown): OpenAiFunctionCall {
|
|
55
|
+
if (raw == null || typeof raw !== 'object') {
|
|
56
|
+
throw new TypeError('parseFunctionCall: input is not an object');
|
|
57
|
+
}
|
|
58
|
+
const r = raw as Record<string, unknown>;
|
|
59
|
+
|
|
60
|
+
// Chat Completions shape
|
|
61
|
+
if (r.function && typeof r.function === 'object') {
|
|
62
|
+
const f = r.function as Record<string, unknown>;
|
|
63
|
+
return {
|
|
64
|
+
id: String(r.id ?? ''),
|
|
65
|
+
name: String(f.name ?? ''),
|
|
66
|
+
arguments: parseArgs(f.arguments),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Responses API or already-normalized
|
|
71
|
+
return {
|
|
72
|
+
id: String(r.id ?? r.call_id ?? ''),
|
|
73
|
+
name: String(r.name ?? ''),
|
|
74
|
+
arguments: parseArgs(r.arguments),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseArgs(args: unknown): Record<string, unknown> {
|
|
79
|
+
if (args == null) return {};
|
|
80
|
+
if (typeof args === 'object') return args as Record<string, unknown>;
|
|
81
|
+
if (typeof args === 'string') {
|
|
82
|
+
try {
|
|
83
|
+
const v = JSON.parse(args);
|
|
84
|
+
if (v && typeof v === 'object') return v as Record<string, unknown>;
|
|
85
|
+
return { _raw: args };
|
|
86
|
+
} catch {
|
|
87
|
+
return { _raw: args };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { _raw: String(args) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// Canonicalization
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Produce the canonical bytes for an OpenAI function call. The bytes are
|
|
99
|
+
* what the agent-action envelope's `content.hash` is a SHA-256 of.
|
|
100
|
+
*
|
|
101
|
+
* Shape (RFC 8785 canonical JSON, then trailing LF):
|
|
102
|
+
* {
|
|
103
|
+
* "arguments": <canonical JSON of arguments>,
|
|
104
|
+
* "id": <call id>,
|
|
105
|
+
* "name": <function name>
|
|
106
|
+
* }
|
|
107
|
+
*/
|
|
108
|
+
export function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array {
|
|
109
|
+
const canon = {
|
|
110
|
+
arguments: c.arguments,
|
|
111
|
+
id: c.id,
|
|
112
|
+
name: c.name,
|
|
113
|
+
};
|
|
114
|
+
const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);
|
|
115
|
+
return new TextEncoder().encode(str + '\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function functionCallHash(c: OpenAiFunctionCall): string {
|
|
119
|
+
return 'sha256:' + hexEncode(sha256(canonicalizeFunctionCall(c)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function functionCallActionId(
|
|
123
|
+
agentAddress: string,
|
|
124
|
+
signedAt: string,
|
|
125
|
+
delegationId: string,
|
|
126
|
+
scopeExercised: string,
|
|
127
|
+
c: OpenAiFunctionCall
|
|
128
|
+
): string {
|
|
129
|
+
return computeActionId({
|
|
130
|
+
address: agentAddress,
|
|
131
|
+
content_hash: functionCallHash(c),
|
|
132
|
+
content_length: canonicalizeFunctionCall(c).byteLength,
|
|
133
|
+
content_mime: 'application/vnd.oc-agent.openai-function-call+json',
|
|
134
|
+
signed_at: signedAt,
|
|
135
|
+
delegation_id: delegationId,
|
|
136
|
+
scope_exercised: scopeExercised,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
141
|
+
// Stamping + invocation
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
export interface StampFunctionCallInput {
|
|
145
|
+
agent: SignerRef;
|
|
146
|
+
delegation: DelegationEnvelope;
|
|
147
|
+
/** Either a normalized `OpenAiFunctionCall` or a raw OpenAI tool_call / function_call object. */
|
|
148
|
+
call: OpenAiFunctionCall | unknown;
|
|
149
|
+
/** Defaults to `openai:function(name=<name>)` — the tightest admissible sub-scope. */
|
|
150
|
+
scopeExercised?: string;
|
|
151
|
+
signedAt?: Date;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function stampFunctionCall(
|
|
155
|
+
input: StampFunctionCallInput
|
|
156
|
+
): Promise<ActionEnvelope> {
|
|
157
|
+
const fc =
|
|
158
|
+
typeof input.call === 'object' &&
|
|
159
|
+
input.call !== null &&
|
|
160
|
+
'arguments' in (input.call as Record<string, unknown>) &&
|
|
161
|
+
typeof (input.call as Record<string, unknown>).arguments !== 'string'
|
|
162
|
+
? (input.call as OpenAiFunctionCall)
|
|
163
|
+
: parseFunctionCall(input.call);
|
|
164
|
+
|
|
165
|
+
const scopeExercised =
|
|
166
|
+
input.scopeExercised ?? `openai:function(name=${fc.name})`;
|
|
167
|
+
|
|
168
|
+
const granted = (input.delegation.scopes ?? []).map(parseScope);
|
|
169
|
+
const exercisedParsed = parseScope(scopeExercised);
|
|
170
|
+
if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const bytes = canonicalizeFunctionCall(fc);
|
|
177
|
+
return signAsAgent({
|
|
178
|
+
agent: input.agent,
|
|
179
|
+
delegation: input.delegation as never,
|
|
180
|
+
content: { hash: functionCallHash(fc), length: bytes.byteLength },
|
|
181
|
+
mime: 'application/vnd.oc-agent.openai-function-call+json',
|
|
182
|
+
scopeExercised,
|
|
183
|
+
signedAt: input.signedAt,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface InvokeWithStampInput<T> extends StampFunctionCallInput {
|
|
188
|
+
/** Your handler. Receives the normalized OpenAiFunctionCall. */
|
|
189
|
+
execute: (call: OpenAiFunctionCall) => Promise<T>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface InvokeWithStampResult<T> {
|
|
193
|
+
result: T;
|
|
194
|
+
action: ActionEnvelope;
|
|
195
|
+
call: OpenAiFunctionCall;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function invokeWithStamp<T>(
|
|
199
|
+
input: InvokeWithStampInput<T>
|
|
200
|
+
): Promise<InvokeWithStampResult<T>> {
|
|
201
|
+
const action = await stampFunctionCall(input);
|
|
202
|
+
const fc = parseFunctionCall(input.call);
|
|
203
|
+
const result = await input.execute(fc);
|
|
204
|
+
return { result, action, call: fc };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
// Console integration: POST stamped actions to console.ochk.io/api/actions
|
|
209
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export interface ConsoleClient {
|
|
212
|
+
/** Defaults to https://console.ochk.io. */
|
|
213
|
+
baseUrl?: string;
|
|
214
|
+
/** Bearer token from /settings § 03 (`ock_<hex>`). */
|
|
215
|
+
apiToken: string;
|
|
216
|
+
/** Project the action belongs to (proj_*). */
|
|
217
|
+
projectId: string;
|
|
218
|
+
/** Optional fetch override for runtimes that need it. */
|
|
219
|
+
fetch?: typeof fetch;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface PostActionResult {
|
|
223
|
+
id: string;
|
|
224
|
+
project_id: string;
|
|
225
|
+
delegation_id: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* POST a stamped action envelope to console.ochk.io/api/actions. The
|
|
230
|
+
* console re-derives the action id, validates agent-must-match-
|
|
231
|
+
* delegation, persists, fans out to Nostr (kind 30084), submits to
|
|
232
|
+
* OC Stamp, and triggers any subscribed webhooks. Throws on non-2xx
|
|
233
|
+
* with the server's reason string.
|
|
234
|
+
*/
|
|
235
|
+
export async function postActionToConsole(
|
|
236
|
+
action: ActionEnvelope,
|
|
237
|
+
client: ConsoleClient
|
|
238
|
+
): Promise<PostActionResult> {
|
|
239
|
+
const baseUrl = client.baseUrl ?? 'https://console.ochk.io';
|
|
240
|
+
const f = client.fetch ?? fetch;
|
|
241
|
+
const body = {
|
|
242
|
+
project_id: client.projectId,
|
|
243
|
+
delegation_id: action.delegation_id,
|
|
244
|
+
agent_address: action.signer.address,
|
|
245
|
+
scope_exercised: action.scope_exercised,
|
|
246
|
+
content_hash: action.content.hash,
|
|
247
|
+
content_length: action.content.length,
|
|
248
|
+
content_mime: action.content.mime,
|
|
249
|
+
signed_at: action.signed_at,
|
|
250
|
+
signature: action.sig.value,
|
|
251
|
+
id: action.id,
|
|
252
|
+
};
|
|
253
|
+
const r = await f(`${baseUrl}/api/actions`, {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: {
|
|
256
|
+
'Content-Type': 'application/json',
|
|
257
|
+
Authorization: `Bearer ${client.apiToken}`,
|
|
258
|
+
},
|
|
259
|
+
body: JSON.stringify(body),
|
|
260
|
+
});
|
|
261
|
+
if (!r.ok) {
|
|
262
|
+
let reason = `http_${r.status}`;
|
|
263
|
+
try {
|
|
264
|
+
const j = (await r.json()) as { reason?: string };
|
|
265
|
+
if (j.reason) reason = j.reason;
|
|
266
|
+
} catch {
|
|
267
|
+
// body wasn't json
|
|
268
|
+
}
|
|
269
|
+
throw new Error(`postActionToConsole failed: ${reason}`);
|
|
270
|
+
}
|
|
271
|
+
const j = (await r.json()) as { ok: true; action: PostActionResult };
|
|
272
|
+
return j.action;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
|
|
276
|
+
/** Console credentials. If absent, behaves like invokeWithStamp. */
|
|
277
|
+
console?: ConsoleClient;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
281
|
+
/** Set when console is configured AND the POST succeeded. */
|
|
282
|
+
posted: PostActionResult | null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function invokeWithStampAndPost<T>(
|
|
286
|
+
input: InvokeWithStampAndPostInput<T>
|
|
287
|
+
): Promise<InvokeWithStampAndPostResult<T>> {
|
|
288
|
+
const { result, action, call } = await invokeWithStamp(input);
|
|
289
|
+
let posted: PostActionResult | null = null;
|
|
290
|
+
if (input.console) {
|
|
291
|
+
try {
|
|
292
|
+
posted = await postActionToConsole(action, input.console);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
// eslint-disable-next-line no-console
|
|
295
|
+
console.error('[oc-agent-openai] postActionToConsole failed:', err);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { result, action, call, posted };
|
|
299
|
+
}
|