@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 CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.4.3 — 2026-10-08
2
+
3
+ - Desktop 0.1.13 release fixes: see commits a2659d24 (admin), 5695a7c3 (chat, agent-runtime), 8ad4ac5c (devops).
4
+
1
5
  ## 0.4.2 — 2026-10-08
2
6
 
3
7
  - Tovu clean-up release: code Tovu moved into Jini, plus the Jini clean-up (see the commit log).
@@ -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
@@ -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;
@@ -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
- usage: obj.usage ?? null,
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.2",
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.2"
82
+ "@jini-ai/protocol": "^0.4.3"
83
83
  },
84
84
  "dependencies": {
85
85
  "@jini-ai/core": "^0.4.2"
@@ -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
@@ -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