@jini-ai/agent-runtime 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/claude-segment-usage.d.ts +19 -0
- package/dist/claude-segment-usage.js +47 -0
- package/dist/claude-stream.d.ts +2 -0
- package/dist/claude-stream.js +15 -4
- package/dist/model-discovery-adapters.d.ts +4 -1
- package/dist/model-discovery-adapters.js +6 -2
- package/package.json +2 -2
- package/dist/defs/gemini.d.ts +0 -24
- package/dist/defs/gemini.js +0 -81
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Claude result totals are per segment. Aborted results can zero counters already streamed. */
|
|
2
|
+
export declare function createClaudeSegmentUsage(_required: Record<string, never>, { nowMs }?: {
|
|
3
|
+
nowMs?: () => number;
|
|
4
|
+
}): {
|
|
5
|
+
observe: ({ messageId, usage }: {
|
|
6
|
+
messageId: string;
|
|
7
|
+
usage: unknown;
|
|
8
|
+
}, _optional?: {}) => void;
|
|
9
|
+
finish: ({ result, interrupted }: {
|
|
10
|
+
result: Record<string, unknown>;
|
|
11
|
+
interrupted: boolean;
|
|
12
|
+
}, _optional?: {}) => {
|
|
13
|
+
usage: {} | null;
|
|
14
|
+
costUsd: {} | null;
|
|
15
|
+
durationMs: {} | null;
|
|
16
|
+
};
|
|
17
|
+
hasObserved: () => boolean;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=claude-segment-usage.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Claude result totals are per segment. Aborted results can zero counters already streamed. */
|
|
2
|
+
export function createClaudeSegmentUsage(_required, { nowMs = Date.now } = {}) {
|
|
3
|
+
const messages = new Map();
|
|
4
|
+
let startedAt = nowMs();
|
|
5
|
+
/** Merge cumulative counters for one message; wrappers repeat the same streamed usage.
|
|
6
|
+
* O(k) per snapshot in k reported counters; retained state is O(m·k) for m messages in a segment. */
|
|
7
|
+
function observe({ messageId, usage }, _optional = {}) {
|
|
8
|
+
if (!usage || typeof usage !== 'object')
|
|
9
|
+
return;
|
|
10
|
+
const previous = messages.get(messageId) ?? {};
|
|
11
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
12
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
|
|
13
|
+
previous[key] = Math.max(previous[key] ?? 0, value);
|
|
14
|
+
}
|
|
15
|
+
if (Object.keys(previous).length)
|
|
16
|
+
messages.set(messageId, previous);
|
|
17
|
+
}
|
|
18
|
+
/** Produce one segment's totals and release its snapshots. Never infer a monetary charge
|
|
19
|
+
* from token counts: only CLI-reported costs are eligible. O(m·k), no external effects. */
|
|
20
|
+
function finish({ result, interrupted }, _optional = {}) {
|
|
21
|
+
const usage = { ...((result.usage && typeof result.usage === 'object') ? result.usage : {}) };
|
|
22
|
+
const observed = {};
|
|
23
|
+
for (const counters of messages.values()) {
|
|
24
|
+
for (const [key, value] of Object.entries(counters))
|
|
25
|
+
observed[key] = (observed[key] ?? 0) + value;
|
|
26
|
+
}
|
|
27
|
+
for (const [key, value] of Object.entries(observed)) {
|
|
28
|
+
const reported = usage[key];
|
|
29
|
+
usage[key] = typeof reported === 'number' && Number.isFinite(reported) ? Math.max(reported, value) : value;
|
|
30
|
+
}
|
|
31
|
+
const modelCosts = result.modelUsage && typeof result.modelUsage === 'object' ? Object.values(result.modelUsage) : [];
|
|
32
|
+
const reportedModelCost = modelCosts.reduce((sum, model) => {
|
|
33
|
+
const cost = model && typeof model === 'object' ? model.costUSD : undefined;
|
|
34
|
+
return sum + (typeof cost === 'number' && Number.isFinite(cost) && cost >= 0 ? cost : 0);
|
|
35
|
+
}, 0);
|
|
36
|
+
const costUsd = reportedModelCost > 0 ? Math.max(typeof result.total_cost_usd === 'number' ? result.total_cost_usd : 0, reportedModelCost) : result.total_cost_usd ?? null;
|
|
37
|
+
const endedAt = nowMs();
|
|
38
|
+
const duration = result.duration_ms;
|
|
39
|
+
const durationMs = typeof duration === 'number' && duration > 0 ? duration
|
|
40
|
+
: messages.size || interrupted ? Math.max(0, endedAt - startedAt) : duration ?? null;
|
|
41
|
+
messages.clear();
|
|
42
|
+
startedAt = endedAt;
|
|
43
|
+
return { usage: Object.keys(usage).length ? usage : result.usage ?? null, costUsd, durationMs };
|
|
44
|
+
}
|
|
45
|
+
return { observe, finish, hasObserved: () => messages.size > 0 };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=claude-segment-usage.js.map
|
package/dist/claude-stream.d.ts
CHANGED
|
@@ -157,6 +157,8 @@ export declare function applyInputJsonDelta(state: BlockState | undefined, parti
|
|
|
157
157
|
export declare function emitAssistantErrorIfPresent(obj: Record<string, unknown>, content: unknown[], onEvent: EventSink): void;
|
|
158
158
|
interface ClaudeStreamHandlerOptions {
|
|
159
159
|
suppressHtmlArtifactsAfterFileWrite?: boolean;
|
|
160
|
+
/** Clock used only when an interrupted segment omits its duration. */
|
|
161
|
+
nowMs?: () => number;
|
|
160
162
|
}
|
|
161
163
|
export declare function createClaudeStreamHandler({ onEvent }: {
|
|
162
164
|
onEvent: EventSink;
|
package/dist/claude-stream.js
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* exercised only through `feed`/`flush` in the test suite.
|
|
38
38
|
*/
|
|
39
39
|
import { createRoleMarkerGuard } from './role-marker-guard.js';
|
|
40
|
+
import { createClaudeSegmentUsage } from './claude-segment-usage.js';
|
|
40
41
|
function isRecord(value) {
|
|
41
42
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
42
43
|
}
|
|
@@ -338,9 +339,10 @@ export function emitAssistantErrorIfPresent(obj, content, onEvent) {
|
|
|
338
339
|
}
|
|
339
340
|
export function createClaudeStreamHandler({ onEvent }, options = {}) {
|
|
340
341
|
let buffer = '';
|
|
342
|
+
const segmentUsage = createClaudeSegmentUsage({}, options.nowMs === undefined ? {} : { nowMs: options.nowMs });
|
|
341
343
|
// Per-content-block scratch, keyed by `${messageId}:${blockIndex}`.
|
|
342
344
|
const blocks = new Map();
|
|
343
|
-
// Tool uses already emitted from streamed `input_json_delta` data.
|
|
345
|
+
// Tool uses already emitted from streamed `input_json_delta` data OR full wrappers.
|
|
344
346
|
// Claude Code still repeats them in the final assistant wrapper, often with
|
|
345
347
|
// empty `{}` inputs, so we suppress that duplicate emission.
|
|
346
348
|
const streamedToolUseIds = new Set();
|
|
@@ -404,6 +406,11 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
|
|
|
404
406
|
}
|
|
405
407
|
}
|
|
406
408
|
function emitToolUse(id, name, input) {
|
|
409
|
+
if (typeof id === 'string') {
|
|
410
|
+
if (streamedToolUseIds.has(id))
|
|
411
|
+
return;
|
|
412
|
+
streamedToolUseIds.add(id);
|
|
413
|
+
}
|
|
407
414
|
if (emitCanonicalTaskSnapshot(id, name, input, taskRegistry, onEvent))
|
|
408
415
|
return;
|
|
409
416
|
if (isFileWriteToolUse(name, input)) {
|
|
@@ -566,6 +573,9 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
|
|
|
566
573
|
}
|
|
567
574
|
}
|
|
568
575
|
flushPendingArtifactText();
|
|
576
|
+
// A hard cancel has no result frame. Flush only counters actually observed, once.
|
|
577
|
+
if (segmentUsage.hasObserved())
|
|
578
|
+
onEvent({ type: 'usage', ...segmentUsage.finish({ result: {}, interrupted: true }), stopReason: null });
|
|
569
579
|
}
|
|
570
580
|
function handleToolUseBlock(block) {
|
|
571
581
|
if (typeof block.id === 'string' && streamedToolUseIds.has(block.id))
|
|
@@ -625,6 +635,7 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
|
|
|
625
635
|
if (typeof message.model === 'string')
|
|
626
636
|
onEvent({ type: 'status', label: 'model', model: message.model });
|
|
627
637
|
const { textMsgId, thinkingMsgId, textAlreadyStreamed, thinkingAlreadyStreamed } = resolveAssistantMessageIds(message);
|
|
638
|
+
segmentUsage.observe({ messageId: currentMessageId ?? `anon${anonymousMessageEpoch}`, usage: message.usage });
|
|
628
639
|
// Per-turn `stop_reason` is emitted as `turn_end` AFTER the content
|
|
629
640
|
// blocks have been processed (see below). When `--include-partial-
|
|
630
641
|
// messages` is unsupported, tool_use events surface only from the
|
|
@@ -664,9 +675,7 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
|
|
|
664
675
|
}
|
|
665
676
|
onEvent({
|
|
666
677
|
type: 'usage',
|
|
667
|
-
|
|
668
|
-
costUsd: obj.total_cost_usd ?? null,
|
|
669
|
-
durationMs: obj.duration_ms ?? null,
|
|
678
|
+
...segmentUsage.finish({ result: obj, interrupted: stopReason?.startsWith('aborted') === true }),
|
|
670
679
|
stopReason,
|
|
671
680
|
});
|
|
672
681
|
}
|
|
@@ -703,6 +712,7 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
|
|
|
703
712
|
// New message boundary — see `anonymousMessageEpoch`'s declaration.
|
|
704
713
|
if (currentMessageId === null)
|
|
705
714
|
anonymousMessageEpoch += 1;
|
|
715
|
+
segmentUsage.observe({ messageId: currentMessageId ?? `anon${anonymousMessageEpoch}`, usage: isRecord(ev.message) ? ev.message.usage : undefined });
|
|
706
716
|
currentMessageStreamedText = false;
|
|
707
717
|
currentMessageStreamedThinking = false;
|
|
708
718
|
if (typeof ev.ttft_ms === 'number') {
|
|
@@ -786,6 +796,7 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
|
|
|
786
796
|
handleContentBlockStop(ev);
|
|
787
797
|
return;
|
|
788
798
|
case 'message_delta':
|
|
799
|
+
segmentUsage.observe({ messageId: currentMessageId ?? `anon${anonymousMessageEpoch}`, usage: ev.usage ?? (isRecord(ev.delta) ? ev.delta.usage : undefined) });
|
|
789
800
|
if (isRecord(ev.delta))
|
|
790
801
|
handleMessageDeltaEvent(ev.delta);
|
|
791
802
|
return;
|
|
@@ -5,7 +5,10 @@ export declare function modelProbeTimeoutMs(agentId: string): number;
|
|
|
5
5
|
export declare function probeCliSubcommand(context: ModelDiscoveryContext, config: AgentDiscoveryConfiguration, deps: ModelDiscoveryDeps, signal: AbortSignal): Promise<ModelProbeResult>;
|
|
6
6
|
/** ACP can create CLI session state/start services. It never sends a user prompt. */
|
|
7
7
|
export declare function probeAcpMetadata(context: ModelDiscoveryContext, args: readonly string[], deps: ModelDiscoveryDeps, signal: AbortSignal): Promise<ModelProbeResult>;
|
|
8
|
-
/** Keep selection IDs, resolved concrete IDs and display names, including capability variants.
|
|
8
|
+
/** Keep selection IDs, resolved concrete IDs and display names, including capability variants.
|
|
9
|
+
* Default-policy display names become the exact resolved ID; malformed responses return null.
|
|
10
|
+
* @complexity O(s + n) time and space in stdout length s and listed models n.
|
|
11
|
+
*/
|
|
9
12
|
export declare function parseClaudeInitializeMetadata(stdout: string): ModelProbeResult | null;
|
|
10
13
|
/** Shared initialize category; CodeBuddy compatibility remains UNVERIFIED locally. */
|
|
11
14
|
export declare function probeClaudeInitialize(context: ModelDiscoveryContext, deps: ModelDiscoveryDeps, signal: AbortSignal): Promise<ModelProbeResult>;
|
|
@@ -39,7 +39,10 @@ export async function probeCliSubcommand(context, config, deps, signal) {
|
|
|
39
39
|
export async function probeAcpMetadata(context, args, deps, signal) {
|
|
40
40
|
return deps.acp.probe({ context, args, signal });
|
|
41
41
|
}
|
|
42
|
-
/** Keep selection IDs, resolved concrete IDs and display names, including capability variants.
|
|
42
|
+
/** Keep selection IDs, resolved concrete IDs and display names, including capability variants.
|
|
43
|
+
* Default-policy display names become the exact resolved ID; malformed responses return null.
|
|
44
|
+
* @complexity O(s + n) time and space in stdout length s and listed models n.
|
|
45
|
+
*/
|
|
43
46
|
export function parseClaudeInitializeMetadata(stdout) {
|
|
44
47
|
for (const line of stdout.split('\n')) {
|
|
45
48
|
let obj;
|
|
@@ -64,9 +67,10 @@ export function parseClaudeInitializeMetadata(stdout) {
|
|
|
64
67
|
const resolvedId = typeof row.resolvedModel === 'string' ? row.resolvedModel.trim() : undefined;
|
|
65
68
|
const label = typeof row.displayName === 'string' && row.displayName.trim() ? row.displayName.trim() : resolvedId || id;
|
|
66
69
|
if (id === 'default') {
|
|
70
|
+
// This displayName describes the CLI's default policy, not the model it resolves to.
|
|
67
71
|
if (resolvedId && modelIdentityKind(resolvedId) === 'concrete') {
|
|
68
72
|
defaultSelectionId = resolvedId;
|
|
69
|
-
models.push({ id: resolvedId, label, identityKind: 'concrete' });
|
|
73
|
+
models.push({ id: resolvedId, label: resolvedId, identityKind: 'concrete' });
|
|
70
74
|
}
|
|
71
75
|
continue;
|
|
72
76
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/agent-runtime",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Coding-agent runtime adapters: runtime registry, detection, launch, stream parsers, and the ACP/pi-rpc subprocess transport.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"@jini-ai/core": "^0.4.0",
|
|
80
80
|
"@jini-ai/oauth": "^0.2.1",
|
|
81
81
|
"@jini-ai/platform": "^0.5.2",
|
|
82
|
-
"@jini-ai/protocol": "^0.4.
|
|
82
|
+
"@jini-ai/protocol": "^0.4.3"
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
85
|
"@jini-ai/core": "^0.4.2"
|
package/dist/defs/gemini.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export declare const geminiAgentDef: {
|
|
2
|
-
id: string;
|
|
3
|
-
name: string;
|
|
4
|
-
bin: string;
|
|
5
|
-
versionArgs: string[];
|
|
6
|
-
versionProbeTimeoutMs: number;
|
|
7
|
-
helpArgs: string[];
|
|
8
|
-
capabilityFlags: {
|
|
9
|
-
"--skip-trust": string;
|
|
10
|
-
};
|
|
11
|
-
fallbackModels: import("../types.js").RuntimeModelOption[];
|
|
12
|
-
buildArgs: ({ prompt: _prompt, imagePaths: _imagePaths }: {
|
|
13
|
-
prompt: string;
|
|
14
|
-
imagePaths: string[];
|
|
15
|
-
}, { extraAllowedDirs, options }?: {
|
|
16
|
-
extraAllowedDirs?: string[];
|
|
17
|
-
options?: import("../types.js").RuntimeBuildOptions;
|
|
18
|
-
runtimeContext?: import("../types.js").RuntimeContext;
|
|
19
|
-
} | undefined) => string[];
|
|
20
|
-
promptViaStdin: true;
|
|
21
|
-
streamFormat: string;
|
|
22
|
-
eventParser: string;
|
|
23
|
-
};
|
|
24
|
-
//# sourceMappingURL=gemini.d.ts.map
|
package/dist/defs/gemini.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module defs/gemini
|
|
3
|
-
*
|
|
4
|
-
* Google's Gemini CLI (`gemini`, npm `@google/gemini-cli`). Checked against an installed
|
|
5
|
-
* `gemini` 0.58.0 (`gemini --help` and the bundled headless code path) and the upstream headless
|
|
6
|
-
* docs (<https://geminicli.com/docs/cli/headless>):
|
|
7
|
-
*
|
|
8
|
-
* - Headless mode starts whenever stdin is not a TTY; the whole piped stdin becomes the prompt
|
|
9
|
-
* (`-p` is only needed to pass the prompt in argv, and would be "appended to input on stdin").
|
|
10
|
-
* So the prompt goes over stdin, like the Qwen Code fork, which avoids Windows `spawn
|
|
11
|
-
* ENAMETOOLONG` on large composed prompts.
|
|
12
|
-
* - `--output-format stream-json` emits the JSONL `init` / `message` / `tool_use` /
|
|
13
|
-
* `tool_result` / `error` / `result` events that `json-event-stream.ts`'s `gemini` parser
|
|
14
|
-
* already reads.
|
|
15
|
-
* - `--approval-mode yolo` auto-approves every tool (the `--yolo` alias does the same).
|
|
16
|
-
* - Since folder trust landed, a headless run in an untrusted directory exits with "Gemini CLI
|
|
17
|
-
* is not running in a trusted directory. To proceed, either use `--skip-trust`, …". The host
|
|
18
|
-
* already chose the run's cwd, so the def trusts it for this session only. Older builds
|
|
19
|
-
* reject unknown flags, so `--skip-trust` is gated on the `--help` capability probe.
|
|
20
|
-
* - `--include-directories` (repeatable) widens the workspace to dirs outside the cwd.
|
|
21
|
-
*
|
|
22
|
-
* Auth: since 2026-06-18 Google no longer serves Gemini CLI's "Login with Google" for free /
|
|
23
|
-
* Google AI Pro / Ultra individual accounts (the run dies with `IneligibleTierError: This client
|
|
24
|
-
* is no longer supported for Gemini Code Assist for individuals`). A Gemini API key, Vertex AI,
|
|
25
|
-
* or a Gemini Code Assist license still works. There is no side-effect-free auth-status
|
|
26
|
-
* subcommand, so no `authProbe`; `auth.ts#classifyAgentAuthFailure` turns the failure text into
|
|
27
|
-
* setup guidance instead.
|
|
28
|
-
*/
|
|
29
|
-
import { DEFAULT_MODEL_OPTION } from './shared.js';
|
|
30
|
-
import { agentCapabilities } from '../capabilities.js';
|
|
31
|
-
const SKIP_TRUST_FLAG = '--skip-trust';
|
|
32
|
-
export const geminiAgentDef = {
|
|
33
|
-
id: 'gemini',
|
|
34
|
-
name: 'Gemini CLI',
|
|
35
|
-
bin: 'gemini',
|
|
36
|
-
versionArgs: ['--version'],
|
|
37
|
-
// A cold `gemini --version` took 4.7 s on the dev machine (warm ~2 s), past the 3 s default.
|
|
38
|
-
versionProbeTimeoutMs: 10_000,
|
|
39
|
-
helpArgs: ['--help'],
|
|
40
|
-
capabilityFlags: {
|
|
41
|
-
[SKIP_TRUST_FLAG]: 'skipTrust',
|
|
42
|
-
},
|
|
43
|
-
// No `models` subcommand. `auto` and the concrete ids below are the ones the installed
|
|
44
|
-
// 0.58.0 bundle and the upstream model docs name; any other id can still be typed in.
|
|
45
|
-
fallbackModels: [
|
|
46
|
-
DEFAULT_MODEL_OPTION,
|
|
47
|
-
{ id: 'auto', label: 'auto' },
|
|
48
|
-
{ id: 'gemini-3.1-pro-preview', label: 'gemini-3.1-pro-preview' },
|
|
49
|
-
{ id: 'gemini-3.5-flash', label: 'gemini-3.5-flash' },
|
|
50
|
-
{ id: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' },
|
|
51
|
-
{ id: 'gemini-3-flash-preview', label: 'gemini-3-flash-preview' },
|
|
52
|
-
{ id: 'gemini-2.5-pro', label: 'gemini-2.5-pro' },
|
|
53
|
-
{ id: 'gemini-2.5-flash', label: 'gemini-2.5-flash' },
|
|
54
|
-
],
|
|
55
|
-
buildArgs: ({ prompt: _prompt, imagePaths: _imagePaths }, { extraAllowedDirs = [], options = {} } = {}) => {
|
|
56
|
-
const args = ['--output-format', 'stream-json'];
|
|
57
|
-
if (agentCapabilities.get('gemini')?.skipTrust) {
|
|
58
|
-
args.push(SKIP_TRUST_FLAG);
|
|
59
|
-
}
|
|
60
|
-
// See `RuntimeBuildOptions.permissionMode`'s doc: bypass is the default (unchanged
|
|
61
|
-
// behavior) unless a caller explicitly opts into a restricted run.
|
|
62
|
-
if (options.permissionMode !== 'restricted') {
|
|
63
|
-
args.push('--approval-mode', 'yolo');
|
|
64
|
-
}
|
|
65
|
-
if (options.model && options.model !== 'default') {
|
|
66
|
-
args.push('--model', options.model);
|
|
67
|
-
}
|
|
68
|
-
const dirs = (extraAllowedDirs || []).filter((d) => typeof d === 'string' && d.length > 0);
|
|
69
|
-
for (const d of dirs)
|
|
70
|
-
args.push('--include-directories', d);
|
|
71
|
-
return args;
|
|
72
|
-
},
|
|
73
|
-
promptViaStdin: true,
|
|
74
|
-
streamFormat: 'json-event-stream',
|
|
75
|
-
eventParser: 'gemini',
|
|
76
|
-
// No `externalMcpInjection` yet: Gemini CLI reads `mcpServers` from `settings.json` (user,
|
|
77
|
-
// project `.gemini/settings.json`, or a system file named by
|
|
78
|
-
// `GEMINI_CLI_SYSTEM_SETTINGS_PATH`), with no per-run `--mcp-config` flag. A run-scoped
|
|
79
|
-
// system-settings file is the likely strategy, but it is not wired or verified live.
|
|
80
|
-
};
|
|
81
|
-
//# sourceMappingURL=gemini.js.map
|