@mono-agent/agent-runtime 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +147 -69
- package/README.md +83 -20
- package/package.json +3 -6
- package/src/agent/compaction.js +0 -11
- package/src/agent/prompt/skill-index.js +5 -1
- package/src/ai/index.js +7 -1
- package/src/ai/observer.js +48 -13
- package/src/ai/pi-interop.js +156 -0
- package/src/ai/providers/claude-cli.js +2 -13
- package/src/ai/providers/claude-sdk.js +14 -8
- package/src/ai/providers/codex-app.js +235 -56
- package/src/ai/providers/opencode-app.js +168 -3
- package/src/ai/providers/pi-messages.js +0 -8
- package/src/ai/providers/pi-native/compaction-driver.js +106 -10
- package/src/ai/providers/pi-native/result-builder.js +2 -14
- package/src/ai/providers/pi-native/stream-subscriber.js +20 -2
- package/src/ai/providers/pi-native/turn-runner.js +31 -8
- package/src/ai/providers/pi-native.js +2 -5
- package/src/ai/runtime/live-input-events.js +94 -0
- package/src/ai/runtime/registry.js +8 -1
- package/src/ai/runtime/router.js +21 -0
- package/src/ai/types.js +2 -1
- package/src/runtime.js +3 -0
- package/types/agent/compaction.d.ts +0 -2
- package/types/agent/prompt/skill-index.d.ts +3 -0
- package/types/ai/index.d.ts +1 -1
- package/types/ai/observer.d.ts +4 -2
- package/types/ai/pi-interop.d.ts +113 -0
- package/types/ai/providers/claude-cli.d.ts +6 -30
- package/types/ai/providers/claude-sdk.d.ts +2 -9
- package/types/ai/providers/codex-app.d.ts +4 -10
- package/types/ai/providers/pi-messages.d.ts +0 -1
- package/types/ai/providers/pi-native/result-builder.d.ts +3 -11
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +4 -2
- package/types/ai/providers/pi-native/turn-runner.d.ts +1 -1
- package/types/ai/runtime/live-input-events.d.ts +22 -0
- package/types/ai/types.d.ts +10 -2
- package/src/ai/backend.js +0 -17
- package/src/ai/registry.js +0 -5
- package/types/ai/backend.d.ts +0 -57
- package/types/ai/registry.d.ts +0 -1
package/src/ai/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Public surface of the provider layer.
|
|
2
2
|
|
|
3
|
-
export * from "./registry.js";
|
|
4
3
|
export * from "./runtime/model-refs.js";
|
|
5
4
|
export * from "./runtime/registry.js";
|
|
6
5
|
export {
|
|
@@ -13,6 +12,13 @@ export {
|
|
|
13
12
|
} from "./runtime/sessions.js";
|
|
14
13
|
export { createMetricsObserver, createObserverHub } from "./observer.js";
|
|
15
14
|
export { generatePiNativeResponse, piNativeRuntimeBridge } from "./providers/pi-native.js";
|
|
15
|
+
export {
|
|
16
|
+
getPiBuiltinModel,
|
|
17
|
+
listPiBuiltinModels,
|
|
18
|
+
loginPiOAuth,
|
|
19
|
+
reasoningLevelsForPiModel,
|
|
20
|
+
resolvePiOAuthApiKey,
|
|
21
|
+
} from "./pi-interop.js";
|
|
16
22
|
export {
|
|
17
23
|
CLAUDE_SDK_CATALOG_VERSION,
|
|
18
24
|
createClaudeSdkDiscoveryIsolation,
|
package/src/ai/observer.js
CHANGED
|
@@ -82,10 +82,11 @@ function addObserver(list, observer) {
|
|
|
82
82
|
// cache: { hits, misses, hitRatio }, // hitRatio in [0,1]; null if no signal
|
|
83
83
|
// tools: { callsByName: { ... }, errorsByName: { ... } },
|
|
84
84
|
// errors: { total, byKind: { ... } },
|
|
85
|
-
// turns: { count, latencyMsP50, latencyMsP95 },
|
|
85
|
+
// turns: { count, sampleCount, latencyMsP50, latencyMsP95 },
|
|
86
86
|
// approvals: { pending, granted, denied },
|
|
87
87
|
// }
|
|
88
|
-
export function createMetricsObserver({ name = "metrics" } = {}) {
|
|
88
|
+
export function createMetricsObserver({ name = "metrics", maxLatencySamples = 2_048 } = {}) {
|
|
89
|
+
const latencySampleLimit = normalizePositiveInteger(maxLatencySamples, 2_048);
|
|
89
90
|
const state = {
|
|
90
91
|
eventsTotal: 0,
|
|
91
92
|
eventsByType: new Map(),
|
|
@@ -99,7 +100,8 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
|
|
|
99
100
|
errorTotal: 0,
|
|
100
101
|
errorsByKind: new Map(),
|
|
101
102
|
turnLatencies: [],
|
|
102
|
-
|
|
103
|
+
turnLatencyCount: 0,
|
|
104
|
+
pendingTurnStarts: [],
|
|
103
105
|
approvalPending: 0,
|
|
104
106
|
approvalGranted: 0,
|
|
105
107
|
approvalDenied: 0,
|
|
@@ -167,17 +169,24 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
|
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
if (type === "provider_request_started" && event.model) {
|
|
170
|
-
state.
|
|
172
|
+
state.pendingTurnStarts.push({
|
|
173
|
+
key: latencyEventKey(event),
|
|
174
|
+
model: String(event.model),
|
|
175
|
+
timestamp: Number.isFinite(event.timestamp) ? event.timestamp : Date.now(),
|
|
176
|
+
});
|
|
177
|
+
if (state.pendingTurnStarts.length > latencySampleLimit) state.pendingTurnStarts.shift();
|
|
171
178
|
}
|
|
172
179
|
if (type === "provider_request_completed" && event.model) {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
180
|
+
const startIndex = matchingStartIndex(state.pendingTurnStarts, event);
|
|
181
|
+
const started = startIndex === -1 ? undefined : state.pendingTurnStarts.splice(startIndex, 1)[0];
|
|
182
|
+
const explicitDuration = Number(event.durationMs);
|
|
183
|
+
if (Number.isFinite(explicitDuration)) recordTurnLatency(Math.max(0, explicitDuration));
|
|
184
|
+
else if (started !== undefined) {
|
|
185
|
+
recordTurnLatency(Math.max(0, ((Number.isFinite(event.timestamp) ? event.timestamp : Date.now())) - started.timestamp));
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
188
|
if (type === "turn_latency" && Number.isFinite(Number(event.durationMs))) {
|
|
180
|
-
|
|
189
|
+
recordTurnLatency(Math.max(0, Number(event.durationMs)));
|
|
181
190
|
}
|
|
182
191
|
|
|
183
192
|
if (type === "tool_approval_pending") state.approvalPending += 1;
|
|
@@ -187,6 +196,12 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
|
|
|
187
196
|
|
|
188
197
|
function recordMetric() { /* future hook */ }
|
|
189
198
|
|
|
199
|
+
function recordTurnLatency(durationMs) {
|
|
200
|
+
state.turnLatencyCount += 1;
|
|
201
|
+
state.turnLatencies.push(durationMs);
|
|
202
|
+
if (state.turnLatencies.length > latencySampleLimit) state.turnLatencies.shift();
|
|
203
|
+
}
|
|
204
|
+
|
|
190
205
|
function snapshot() {
|
|
191
206
|
const cacheTotal = state.cacheHits + state.cacheMisses;
|
|
192
207
|
const hitRatio = cacheTotal > 0 ? state.cacheHits / cacheTotal : null;
|
|
@@ -205,7 +220,7 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
|
|
|
205
220
|
errorsByName: Object.fromEntries(state.toolErrorsByName),
|
|
206
221
|
},
|
|
207
222
|
errors: { total: state.errorTotal, byKind: Object.fromEntries(state.errorsByKind) },
|
|
208
|
-
turns: percentilesFor(state.turnLatencies),
|
|
223
|
+
turns: percentilesFor(state.turnLatencies, state.turnLatencyCount),
|
|
209
224
|
approvals: { pending: state.approvalPending, granted: state.approvalGranted, denied: state.approvalDenied },
|
|
210
225
|
};
|
|
211
226
|
}
|
|
@@ -213,16 +228,36 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
|
|
|
213
228
|
return { name, recordEvent, recordMetric, snapshot };
|
|
214
229
|
}
|
|
215
230
|
|
|
216
|
-
function percentilesFor(samples) {
|
|
231
|
+
function percentilesFor(samples, count = samples.length) {
|
|
217
232
|
const arr = Array.isArray(samples) ? samples.filter((n) => Number.isFinite(n)).slice().sort((a, b) => a - b) : [];
|
|
218
|
-
if (!arr.length) return { count: 0, latencyMsP50: null, latencyMsP95: null };
|
|
233
|
+
if (!arr.length) return { count, sampleCount: 0, latencyMsP50: null, latencyMsP95: null };
|
|
219
234
|
return {
|
|
220
|
-
count
|
|
235
|
+
count,
|
|
236
|
+
sampleCount: arr.length,
|
|
221
237
|
latencyMsP50: percentile(arr, 0.5),
|
|
222
238
|
latencyMsP95: percentile(arr, 0.95),
|
|
223
239
|
};
|
|
224
240
|
}
|
|
225
241
|
|
|
242
|
+
function latencyEventKey(event) {
|
|
243
|
+
const requestId = event.requestId ?? event.turnId;
|
|
244
|
+
return requestId === undefined || requestId === null ? undefined : String(requestId);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function matchingStartIndex(starts, event) {
|
|
248
|
+
const key = latencyEventKey(event);
|
|
249
|
+
if (key !== undefined) {
|
|
250
|
+
const keyedIndex = starts.findIndex((start) => start.key === key);
|
|
251
|
+
if (keyedIndex !== -1) return keyedIndex;
|
|
252
|
+
}
|
|
253
|
+
return starts.findIndex((start) => start.model === String(event.model));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizePositiveInteger(value, fallback) {
|
|
257
|
+
const number = Number(value);
|
|
258
|
+
return Number.isSafeInteger(number) && number > 0 ? number : fallback;
|
|
259
|
+
}
|
|
260
|
+
|
|
226
261
|
function percentile(sortedArr, q) {
|
|
227
262
|
if (!sortedArr.length) return null;
|
|
228
263
|
const rank = q * (sortedArr.length - 1);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Runtime-owned interoperability facade for Pi's built-in model and OAuth
|
|
2
|
+
// surfaces. Consumers should use these functions instead of importing pi-ai
|
|
3
|
+
// directly so the runtime's known-good Pi version remains authoritative.
|
|
4
|
+
|
|
5
|
+
import { getBuiltinModel, getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
|
|
6
|
+
import { getOAuthApiKey, getOAuthProvider } from "@earendil-works/pi-ai/oauth";
|
|
7
|
+
import { reasoningLevelsForPiModel as resolveReasoningLevels } from "./providers/pi-models.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* id: string,
|
|
12
|
+
* name: string,
|
|
13
|
+
* api: string,
|
|
14
|
+
* provider: string,
|
|
15
|
+
* baseUrl: string,
|
|
16
|
+
* reasoning: boolean,
|
|
17
|
+
* input: Array<"text"|"image">,
|
|
18
|
+
* cost: {
|
|
19
|
+
* input: number,
|
|
20
|
+
* output: number,
|
|
21
|
+
* cacheRead: number,
|
|
22
|
+
* cacheWrite: number,
|
|
23
|
+
* tiers?: Array<{
|
|
24
|
+
* inputTokensAbove: number,
|
|
25
|
+
* input: number,
|
|
26
|
+
* output: number,
|
|
27
|
+
* cacheRead: number,
|
|
28
|
+
* cacheWrite: number
|
|
29
|
+
* }>
|
|
30
|
+
* },
|
|
31
|
+
* contextWindow: number,
|
|
32
|
+
* maxTokens: number,
|
|
33
|
+
* thinkingLevelMap?: Object<string, string|null>,
|
|
34
|
+
* compat?: Object<string, *>,
|
|
35
|
+
* headers?: Object<string, string>,
|
|
36
|
+
* [key: string]: *
|
|
37
|
+
* }} PiBuiltinModelSnapshot
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {"none"|"minimal"|"low"|"medium"|"high"|"xhigh"|"max"} PiReasoningLevel
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {{
|
|
46
|
+
* refresh: string,
|
|
47
|
+
* access: string,
|
|
48
|
+
* expires: number,
|
|
49
|
+
* [key: string]: *
|
|
50
|
+
* }} PiOAuthCredentialsSnapshot
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} PiOAuthLoginCallbacks
|
|
55
|
+
* @property {(info: {url: string, instructions?: string}) => void} onAuth
|
|
56
|
+
* @property {(info: {userCode: string, verificationUri: string, intervalSeconds?: number, expiresInSeconds?: number}) => void} onDeviceCode
|
|
57
|
+
* @property {(prompt: {message: string, placeholder?: string, allowEmpty?: boolean}) => Promise<string>} onPrompt
|
|
58
|
+
* @property {(message: string) => void} [onProgress]
|
|
59
|
+
* @property {() => Promise<string>} [onManualCodeInput]
|
|
60
|
+
* @property {(prompt: {message: string, options: Array<{id: string, label: string}>}) => Promise<string|undefined>} onSelect
|
|
61
|
+
* @property {AbortSignal} [signal]
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clone provider-owned data before it crosses the public runtime boundary.
|
|
66
|
+
* Pi's built-in models and OAuth credentials are structured data on the
|
|
67
|
+
* supported version, and the package requires a Node release with
|
|
68
|
+
* `structuredClone`.
|
|
69
|
+
*
|
|
70
|
+
* @template T
|
|
71
|
+
* @param {T} value
|
|
72
|
+
* @returns {T}
|
|
73
|
+
*/
|
|
74
|
+
function cloneInteropValue(value) {
|
|
75
|
+
return structuredClone(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* List defensive snapshots of Pi's built-in models for one provider.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} providerId
|
|
82
|
+
* @returns {PiBuiltinModelSnapshot[]}
|
|
83
|
+
*/
|
|
84
|
+
export function listPiBuiltinModels(providerId) {
|
|
85
|
+
const models = getBuiltinModels(/** @type {any} */ (providerId));
|
|
86
|
+
return /** @type {PiBuiltinModelSnapshot[]} */ (cloneInteropValue(models));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read a defensive snapshot of one Pi built-in model.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} providerId
|
|
93
|
+
* @param {string} modelId
|
|
94
|
+
* @returns {PiBuiltinModelSnapshot|undefined}
|
|
95
|
+
*/
|
|
96
|
+
export function getPiBuiltinModel(providerId, modelId) {
|
|
97
|
+
const model = getBuiltinModel(
|
|
98
|
+
/** @type {any} */ (providerId),
|
|
99
|
+
/** @type {any} */ (modelId),
|
|
100
|
+
);
|
|
101
|
+
return model === undefined
|
|
102
|
+
? undefined
|
|
103
|
+
: /** @type {PiBuiltinModelSnapshot} */ (cloneInteropValue(model));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Translate Pi's model-native thinking levels to mono-agent effort spelling.
|
|
108
|
+
*
|
|
109
|
+
* @param {PiBuiltinModelSnapshot} model
|
|
110
|
+
* @returns {PiReasoningLevel[]}
|
|
111
|
+
*/
|
|
112
|
+
export function reasoningLevelsForPiModel(model) {
|
|
113
|
+
return /** @type {PiReasoningLevel[]} */ (resolveReasoningLevels(model));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve an OAuth-backed API key without allowing Pi to mutate the caller's
|
|
118
|
+
* credential record or returning Pi-owned credential objects.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} providerId
|
|
121
|
+
* @param {Object<string, PiOAuthCredentialsSnapshot>} credentials
|
|
122
|
+
* @returns {Promise<{apiKey: string, newCredentials: PiOAuthCredentialsSnapshot}|null>}
|
|
123
|
+
*/
|
|
124
|
+
export async function resolvePiOAuthApiKey(providerId, credentials) {
|
|
125
|
+
const result = await getOAuthApiKey(
|
|
126
|
+
providerId,
|
|
127
|
+
/** @type {any} */ (cloneInteropValue(credentials)),
|
|
128
|
+
);
|
|
129
|
+
if (!result) return null;
|
|
130
|
+
return {
|
|
131
|
+
apiKey: result.apiKey,
|
|
132
|
+
newCredentials: cloneInteropValue(result.newCredentials),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run a supported Pi OAuth login flow without exposing Pi's mutable provider
|
|
138
|
+
* registry or provider instances.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} providerId
|
|
141
|
+
* @param {PiOAuthLoginCallbacks} callbacks
|
|
142
|
+
* @returns {Promise<PiOAuthCredentialsSnapshot>}
|
|
143
|
+
*/
|
|
144
|
+
export async function loginPiOAuth(providerId, callbacks) {
|
|
145
|
+
const provider = getOAuthProvider(providerId);
|
|
146
|
+
if (!provider || typeof provider.login !== "function") {
|
|
147
|
+
throw new Error(`Pi OAuth provider is unavailable: ${providerId}`);
|
|
148
|
+
}
|
|
149
|
+
for (const callbackName of ["onAuth", "onDeviceCode", "onPrompt", "onSelect"]) {
|
|
150
|
+
if (typeof callbacks?.[callbackName] !== "function") {
|
|
151
|
+
throw new TypeError(`loginPiOAuth requires callbacks.${callbackName}()`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const credentials = await provider.login(/** @type {any} */ ({ ...callbacks }));
|
|
155
|
+
return cloneInteropValue(credentials);
|
|
156
|
+
}
|
|
@@ -83,7 +83,8 @@ const DORMANT_CLI_CAPABILITIES = {
|
|
|
83
83
|
supports_mcp: true,
|
|
84
84
|
supports_skills: true,
|
|
85
85
|
supports_builtin_tools: true,
|
|
86
|
-
|
|
86
|
+
// The one-shot CLI bridge cannot add stdin messages after process launch.
|
|
87
|
+
supports_live_input: false,
|
|
87
88
|
supports_native_subagents: true,
|
|
88
89
|
};
|
|
89
90
|
|
|
@@ -776,18 +777,6 @@ export async function generateCliResponse(systemPrompt, options = {}) {
|
|
|
776
777
|
}
|
|
777
778
|
}
|
|
778
779
|
|
|
779
|
-
export const claudeCodeBackend = {
|
|
780
|
-
kind: "claude-code",
|
|
781
|
-
capabilities: { kind: "claude-code", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
|
|
782
|
-
execute: generateCliResponse,
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
export const codexCliBackend = {
|
|
786
|
-
kind: "codex-cli",
|
|
787
|
-
capabilities: { kind: "codex-cli", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
|
|
788
|
-
execute: generateCliResponse,
|
|
789
|
-
};
|
|
790
|
-
|
|
791
780
|
// CLI bridge for sdk='claude' agents that opt into execution_mode='cli'.
|
|
792
781
|
// generateCliResponse internally branches on resolved.sdk; the SDK shape
|
|
793
782
|
// from parseModelReference uses 'claude', the CLI builder expects
|
|
@@ -563,7 +563,18 @@ function createClaudeCanUseTool(approvalManager, modelName) {
|
|
|
563
563
|
async function* livePromptMessages({ initialPrompt, liveInput, sessionId, prompts }) {
|
|
564
564
|
yield makeSdkUserMessage(initialPrompt, sessionId);
|
|
565
565
|
for await (const message of liveInput) {
|
|
566
|
-
|
|
566
|
+
try {
|
|
567
|
+
const sdkMessage = makeSdkUserMessage(
|
|
568
|
+
formatLiveInputGuidance(message.body, prompts),
|
|
569
|
+
sessionId,
|
|
570
|
+
message.id || randomUUID(),
|
|
571
|
+
);
|
|
572
|
+
message.acknowledge?.();
|
|
573
|
+
yield sdkMessage;
|
|
574
|
+
} catch (err) {
|
|
575
|
+
message.reject?.(err);
|
|
576
|
+
throw err;
|
|
577
|
+
}
|
|
567
578
|
}
|
|
568
579
|
}
|
|
569
580
|
|
|
@@ -737,6 +748,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
737
748
|
const prompt = options.liveInput
|
|
738
749
|
? livePromptMessages({ initialPrompt: promptString, liveInput: options.liveInput, sessionId: reusableProviderSessionId || randomUUID(), prompts: options.prompts })
|
|
739
750
|
: promptString;
|
|
751
|
+
const claudeAgentQuery = options.claudeAgentQuery ?? query;
|
|
740
752
|
const providerRequestStartedAt = Date.now();
|
|
741
753
|
emitEvent({
|
|
742
754
|
type: "provider_request_started",
|
|
@@ -794,7 +806,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
794
806
|
}
|
|
795
807
|
|
|
796
808
|
try {
|
|
797
|
-
stream =
|
|
809
|
+
stream = claudeAgentQuery({ prompt: /** @type {any} */ (prompt), options: queryOptions });
|
|
798
810
|
for await (const event of stream) {
|
|
799
811
|
const nextSessionId = sessionIdFromEvent(event);
|
|
800
812
|
if (nextSessionId) providerSessionId = nextSessionId;
|
|
@@ -1051,12 +1063,6 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
1051
1063
|
};
|
|
1052
1064
|
}
|
|
1053
1065
|
|
|
1054
|
-
export const claudeSdkBackend = {
|
|
1055
|
-
kind: "claude",
|
|
1056
|
-
capabilities: runtimeCapabilities("claude"),
|
|
1057
|
-
execute: generateClaudeResponse,
|
|
1058
|
-
};
|
|
1059
|
-
|
|
1060
1066
|
export const claudeRuntimeBridge = {
|
|
1061
1067
|
id: "claude",
|
|
1062
1068
|
kind: "claude",
|