super-dsh 0.1.9 → 0.1.10-e

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.
Files changed (178) hide show
  1. package/README.md +25 -3
  2. package/agent-agy/cordis.patch.yml +7 -4
  3. package/agent-agy/dist/agent.d.ts +12 -11
  4. package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
  5. package/agent-agy/dist/agy-auth-gateway.js +233 -0
  6. package/agent-agy/dist/agy-auth.d.ts +153 -0
  7. package/agent-agy/dist/agy-auth.js +179 -0
  8. package/agent-agy/dist/agy-cli-client.d.ts +39 -9
  9. package/agent-agy/dist/agy-cli-client.js +82 -18
  10. package/agent-agy/dist/agy-store.d.ts +8 -0
  11. package/agent-agy/dist/index.d.ts +46 -16
  12. package/agent-agy/dist/index.js +82 -56
  13. package/agent-agy/dist/models.d.ts +70 -0
  14. package/agent-agy/dist/models.js +102 -3
  15. package/agent-agy/package.json +7 -5
  16. package/agent-agy-sdk/cordis.patch.yml +58 -0
  17. package/agent-agy-sdk/dist/adapter.d.ts +15 -0
  18. package/agent-agy-sdk/dist/adapter.js +42 -0
  19. package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
  20. package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
  21. package/agent-agy-sdk/dist/agent.d.ts +138 -0
  22. package/agent-agy-sdk/dist/agent.js +494 -0
  23. package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
  24. package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
  25. package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
  26. package/agent-agy-sdk/dist/agy-sessions.js +107 -0
  27. package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
  28. package/agent-agy-sdk/dist/agy-store.js +83 -0
  29. package/agent-agy-sdk/dist/inbox.d.ts +26 -0
  30. package/agent-agy-sdk/dist/inbox.js +53 -0
  31. package/agent-agy-sdk/dist/index.d.ts +93 -0
  32. package/agent-agy-sdk/dist/index.js +476 -0
  33. package/agent-agy-sdk/dist/models.d.ts +41 -0
  34. package/agent-agy-sdk/dist/models.js +74 -0
  35. package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
  36. package/agent-agy-sdk/dist/sdk-venv.js +173 -0
  37. package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
  38. package/agent-agy-sdk/dist/world-plugin.js +82 -0
  39. package/agent-agy-sdk/package.json +64 -0
  40. package/agent-claude/dist/adapter.js +106 -31
  41. package/agent-claude/dist/agent.js +154 -17
  42. package/agent-claude/dist/claude-client.js +84 -43
  43. package/agent-claude/dist/claude-launch.js +101 -0
  44. package/agent-claude/dist/claude-native-config.js +169 -0
  45. package/agent-claude/dist/claude-provider-gateway.js +340 -0
  46. package/agent-claude/dist/claude-providers.js +391 -0
  47. package/agent-claude/dist/claude-route.js +152 -0
  48. package/agent-claude/dist/index.js +214 -37
  49. package/agent-claude/dist/models.js +14 -0
  50. package/agent-claude/dist/sdk-runtime.js +320 -0
  51. package/agent-claude/package.json +6 -4
  52. package/agent-codex/README.md +141 -36
  53. package/agent-codex/cordis.patch.yml +14 -10
  54. package/agent-codex/dist/adapter.js +64 -28
  55. package/agent-codex/dist/agent.js +255 -81
  56. package/agent-codex/dist/app-server-approvals.js +165 -0
  57. package/agent-codex/dist/app-server-client.js +723 -0
  58. package/agent-codex/dist/app-server-events.js +432 -0
  59. package/agent-codex/dist/app-server-protocol.js +95 -0
  60. package/agent-codex/dist/app-server-wire.js +299 -0
  61. package/agent-codex/dist/codex-launch.js +78 -0
  62. package/agent-codex/dist/codex-native-config.js +392 -0
  63. package/agent-codex/dist/codex-provider-gateway.js +402 -0
  64. package/agent-codex/dist/codex-providers.js +376 -0
  65. package/agent-codex/dist/codex-route.js +86 -0
  66. package/agent-codex/dist/index.js +256 -115
  67. package/agent-codex/dist/permission.js +75 -1
  68. package/agent-codex/package.json +1 -2
  69. package/agent-codex-sdk/README.md +79 -0
  70. package/agent-codex-sdk/cordis.patch.yml +81 -0
  71. package/agent-codex-sdk/dist/adapter.js +91 -0
  72. package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
  73. package/agent-codex-sdk/dist/agent.js +1085 -0
  74. package/agent-codex-sdk/dist/codex-native-config.js +392 -0
  75. package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
  76. package/agent-codex-sdk/dist/codex-providers.js +376 -0
  77. package/agent-codex-sdk/dist/codex-route.js +86 -0
  78. package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
  79. package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
  80. package/agent-codex-sdk/dist/codex-store.js +180 -0
  81. package/agent-codex-sdk/dist/inbox.js +53 -0
  82. package/agent-codex-sdk/dist/index.js +739 -0
  83. package/agent-codex-sdk/dist/permission.js +71 -0
  84. package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
  85. package/agent-codex-sdk/dist/session-map.js +167 -0
  86. package/agent-codex-sdk/dist/world-plugin.js +93 -0
  87. package/agent-codex-sdk/package.json +79 -0
  88. package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
  89. package/agent-omp/README.md +72 -55
  90. package/agent-omp/dist/agent.js +36 -11
  91. package/agent-omp/dist/index.js +92 -52
  92. package/agent-omp/dist/lazy-rpc.js +11 -4
  93. package/agent-omp/dist/omp-cli.js +114 -97
  94. package/agent-omp/dist/omp-store.js +12 -4
  95. package/agent-omp/dist/permission.js +70 -5
  96. package/agent-omp/dist/rpc-client.js +409 -0
  97. package/agent-omp/dist/world-plugin.js +36 -3
  98. package/agent-omp/package.json +2 -6
  99. package/agent-omp-sdk/README.md +103 -0
  100. package/agent-omp-sdk/cordis.patch.yml +108 -0
  101. package/agent-omp-sdk/dist/adapter.d.ts +21 -0
  102. package/agent-omp-sdk/dist/adapter.js +36 -0
  103. package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
  104. package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
  105. package/agent-omp-sdk/dist/agent.d.ts +64 -0
  106. package/agent-omp-sdk/dist/agent.js +1178 -0
  107. package/agent-omp-sdk/dist/discovery.d.ts +57 -0
  108. package/agent-omp-sdk/dist/discovery.js +116 -0
  109. package/agent-omp-sdk/dist/inbox.d.ts +28 -0
  110. package/agent-omp-sdk/dist/inbox.js +53 -0
  111. package/agent-omp-sdk/dist/index.d.ts +60 -0
  112. package/agent-omp-sdk/dist/index.js +632 -0
  113. package/agent-omp-sdk/dist/knobs.d.ts +34 -0
  114. package/agent-omp-sdk/dist/knobs.js +50 -0
  115. package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
  116. package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
  117. package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
  118. package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
  119. package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
  120. package/agent-omp-sdk/dist/mobile-boot.js +38 -0
  121. package/agent-omp-sdk/dist/models.d.ts +39 -0
  122. package/agent-omp-sdk/dist/models.js +299 -0
  123. package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
  124. package/agent-omp-sdk/dist/omp-cli.js +164 -0
  125. package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
  126. package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
  127. package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
  128. package/agent-omp-sdk/dist/omp-executable.js +30 -0
  129. package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
  130. package/agent-omp-sdk/dist/omp-store.js +526 -0
  131. package/agent-omp-sdk/dist/pairing.d.ts +7 -0
  132. package/agent-omp-sdk/dist/pairing.js +45 -0
  133. package/agent-omp-sdk/dist/permission.d.ts +138 -0
  134. package/agent-omp-sdk/dist/permission.js +161 -0
  135. package/agent-omp-sdk/dist/protocol.d.ts +189 -0
  136. package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
  137. package/agent-omp-sdk/dist/rpc-types.js +1 -0
  138. package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
  139. package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
  140. package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
  141. package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
  142. package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
  143. package/agent-omp-sdk/dist/sidecar-client.js +197 -0
  144. package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
  145. package/agent-omp-sdk/dist/store/adapter.js +47 -0
  146. package/agent-omp-sdk/dist/store/db.d.ts +70 -0
  147. package/agent-omp-sdk/dist/store/db.js +210 -0
  148. package/agent-omp-sdk/dist/store/index.d.ts +23 -0
  149. package/agent-omp-sdk/dist/store/index.js +59 -0
  150. package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
  151. package/agent-omp-sdk/dist/store/reconcile.js +51 -0
  152. package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
  153. package/agent-omp-sdk/dist/store/schema.js +40 -0
  154. package/agent-omp-sdk/dist/transport.d.ts +49 -0
  155. package/agent-omp-sdk/dist/transport.js +35 -0
  156. package/agent-omp-sdk/dist/world-home.d.ts +4 -0
  157. package/agent-omp-sdk/dist/world-home.js +23 -0
  158. package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
  159. package/agent-omp-sdk/dist/world-plugin.js +137 -0
  160. package/agent-omp-sdk/package.json +87 -0
  161. package/client/index.js +1204 -93
  162. package/cordis.patch.yml +64 -0
  163. package/package.json +17 -8
  164. package/superd-hub/dist/client/agent-settings.d.ts +210 -0
  165. package/superd-hub/dist/client/agent-settings.js +300 -0
  166. package/superd-hub/dist/client/roster.js +6 -0
  167. package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
  168. package/superd-hub/dist/client/runtime-settings-api.js +137 -0
  169. package/superd-hub/dist/client/switch.d.ts +165 -0
  170. package/superd-hub/dist/client/switch.js +295 -0
  171. package/superd-hub/lib/client/index.js +1204 -93
  172. package/agent-codex/dist/models.js +0 -152
  173. package/agent-omp/dist/sidecar-client.js +0 -195
  174. /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
  175. /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
  176. /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
  177. /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
  178. /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * AgyAgent — the Dash `Agent` shim over one `AgyBridgeClient` (the Python SDK
3
+ * bridge child; localharness materializes inside it on the first prompt).
4
+ *
5
+ * Projection (bridge → Dash session events):
6
+ * (prompt delivery) → turn/start + user/message
7
+ * chunk (streamed) → assistant stream frames (v2 protocol) accumulated
8
+ * as durable records; the FULL text commits once as
9
+ * assistant/message at `done`
10
+ * tool → (V1: bridge supplies name only — omitted from the
11
+ * log rather than fabricated; event-stream honesty)
12
+ * done → assistant/message + step/end + turn/end {completed}
13
+ * error → turn/end {error} with a stable code
14
+ *
15
+ * Bridge gaps registered explicitly (never fabricated):
16
+ * - token usage: the bridge `done` event carries none → no usage field.
17
+ * - tool fidelity: only a tool name arrives (no call id / args / result) → no
18
+ * tool/call or tool/result events in V1.
19
+ * - steer/cancel: the bridge has no in-turn interrupt; `steer` degrades to a
20
+ * parked follow-up, `cancel` records the cause (applied at the terminal).
21
+ *
22
+ * Onboarding (§17 minimal, consumer-first): when no Gemini API key is known,
23
+ * the first user turn never touches the bridge — the turn is answered locally
24
+ * with a system/message asking for the key; a pasted key is persisted through
25
+ * `onApiKey` (adapter state, world home only) and confirmed in the same turn.
26
+ */
27
+ import type { Context } from "@deepseek-ai/cordis";
28
+ import type { Agent, AgentOptions, AgentStatus, CancelOptions, InboxTarget } from "@deepseek-ai/dsh-agent";
29
+ import { Inbox } from "./inbox.js";
30
+ import type { AgentCancelCause, Session, SessionId, UserMessage } from "@deepseek-ai/dsh-session";
31
+ import type { TokenUsage } from "@deepseek-ai/dsh-llm";
32
+ import type { AgyBridgeClient } from "./agy-client.js";
33
+ import { type WithSystemHead } from "../../superd-hub/dist/index.js";
34
+ /** The live transport the agent drives (this package is SDK-only). */
35
+ export type LiveAgyClient = Pick<AgyBridgeClient, "conversationId" | "lastUsage" | "onThinkingDelta" | "onToolEvent" | "setApprovalHandler" | "turn" | "close"> & {
36
+ setSystemMessageHandler?(handler: (text: string) => void): void;
37
+ };
38
+ /** Classify a bridge turn failure into the stable {message, code} UI chip. */
39
+ export declare function classifyTurnError(error: unknown): {
40
+ message: string;
41
+ code: string;
42
+ };
43
+ /** Join the visible text blocks of a Dash user message into the prompt string. */
44
+ export declare function userMessageText(message: UserMessage): string;
45
+ /** Whether a raw user text looks like a pasteable Gemini API key. */
46
+ export declare function looksLikeApiKey(text: string): boolean;
47
+ /** Runtime facts the provider stamps the agent with (mirrors hermes). */
48
+ export interface AgyAgentRuntimeInfo {
49
+ /** Session materialization context for the bridge (first prompt). */
50
+ readonly session: {
51
+ cwd: string;
52
+ };
53
+ /** True when no Gemini API key is known: turns are answered locally. */
54
+ readonly awaitingKey: boolean;
55
+ /** Persist a pasted key (adapter state, world home ONLY). */
56
+ readonly onSaveApiKey: (key: string) => void;
57
+ /** Fired after each bridge turn completes (the provider upserts the conversation id). */
58
+ readonly onTurnDone?: () => void;
59
+ }
60
+ export declare class AgyAgent implements Agent {
61
+ readonly id: SessionId;
62
+ readonly options: AgentOptions;
63
+ readonly session: WithSystemHead<Session>;
64
+ readonly inbox: Inbox;
65
+ readonly ctx: Context;
66
+ private readonly client;
67
+ private readonly loopCtx;
68
+ private readonly scope;
69
+ private readonly dispatch;
70
+ private readonly runtimeInfo;
71
+ private streaming;
72
+ private dashTurn;
73
+ private step;
74
+ private lastTurn;
75
+ private turnOpen;
76
+ private cancelCause;
77
+ private streamBridge;
78
+ private assistantAttemptCounter;
79
+ private assistantStreamRevision;
80
+ private remoteQueue;
81
+ private turnDeadlineTimer;
82
+ private disposed;
83
+ private systemMessageDone;
84
+ private activityDone;
85
+ private resolveActivityDone;
86
+ private readonly onIdleExit;
87
+ constructor(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session, client: LiveAgyClient, onIdleExit?: () => void, runtimeInfo?: AgyAgentRuntimeInfo);
88
+ get status(): AgentStatus;
89
+ send(message: UserMessage, _target: InboxTarget, wakeup: boolean): void;
90
+ followup(message: UserMessage): void;
91
+ /** No native steer surface on the bridge (V1 gap): degrade to a parked follow-up. */
92
+ steer(message: UserMessage): void;
93
+ inject(message: UserMessage): void;
94
+ /** The bridge has no in-turn interrupt (V1 gap): record the cause; applied at the terminal. */
95
+ cancel(cause: AgentCancelCause, _options?: CancelOptions): void;
96
+ whenIdle(): Promise<void>;
97
+ runMaintenance<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>;
98
+ dispose(): Promise<void>;
99
+ private deliver;
100
+ private deliverPrompt;
101
+ private sessionIdentityCommitted;
102
+ /** Stamp the single-preset selection once per session (projection seed). */
103
+ private bootstrapSessionIdentity;
104
+ private startTurn;
105
+ private appendSystemNotice;
106
+ private appendAssistantMessage;
107
+ private ensureStreamBridge;
108
+ private abandonStreamBridge;
109
+ private reserveTurn;
110
+ private closeTurn;
111
+ private closeStep;
112
+ private commitStepStart;
113
+ /** One-shot system stamp: the onboarding notice rides the first turn. */
114
+ private markRunning;
115
+ private markIdle;
116
+ private beginActivity;
117
+ private endActivity;
118
+ /** Flush ONE parked follow-up as a fresh prompt (never submit while busy). */
119
+ private flushFollowUps;
120
+ private pendingThinking;
121
+ /** Calls from this turn still lacking a result (SDK 0.1.17 exposes none). */
122
+ private unpairedCalls;
123
+ private armTurnDeadline;
124
+ private clearTurnDeadline;
125
+ }
126
+ /**
127
+ * Map the bridge's best-effort usage event into Dash `TokenUsage`. The bridge
128
+ * derives it from `conversation.last_turn_usage` AFTER the stream — when it is
129
+ * absent there is NO usage field (never fabricated). `thinking_tokens` rides
130
+ * along only when it is a subset of `output_tokens` (the DSH fold rejects a
131
+ * larger value outright).
132
+ */
133
+ export declare function convertAgyUsage(usage: {
134
+ input_tokens: number;
135
+ output_tokens: number;
136
+ thinking_tokens: number;
137
+ total_tokens: number;
138
+ } | undefined): TokenUsage | undefined;
@@ -0,0 +1,494 @@
1
+ import { agentEvents } from "@deepseek-ai/dsh-agent";
2
+ import { Inbox } from "./inbox.js";
3
+ import { LlmAttemptId, ToolCallId, createAssistantMessage, createSystemMessage, createToolResultMessage } from "@deepseek-ai/dsh-llm";
4
+ import { createScope } from "@deepseek-ai/dsh-scope";
5
+ import { withSystemHead } from "../../superd-hub/dist/index.js";
6
+ /** v2 live assistant-stream publication (same frame protocol as hermes). */
7
+ class AssistantStreamBridge {
8
+ attemptId;
9
+ records = [];
10
+ nextRevision;
11
+ emit;
12
+ index = 0;
13
+ terminal = false;
14
+ constructor(sessionId, attempt, nextRevision, emit) {
15
+ this.attemptId = LlmAttemptId(`${sessionId}:${attempt}`);
16
+ this.nextRevision = nextRevision;
17
+ this.emit = emit;
18
+ }
19
+ get ended() {
20
+ return this.terminal;
21
+ }
22
+ start(turn, step) {
23
+ this.emit({ type: "start", attemptId: this.attemptId, revision: this.nextRevision(), turn, step });
24
+ }
25
+ push(chunk) {
26
+ const time = Date.now();
27
+ this.records.push({ type: "chunk", time, chunk });
28
+ this.emit({ type: "chunk", attemptId: this.attemptId, revision: this.nextRevision(), index: this.index++, time, chunk });
29
+ }
30
+ settle(seq) {
31
+ this.terminal = true;
32
+ this.emit({ type: "end", attemptId: this.attemptId, revision: this.nextRevision(), index: this.index, outcome: { kind: "committed", eventType: "assistant/message", seq } });
33
+ }
34
+ abandon() {
35
+ this.terminal = true;
36
+ this.emit({ type: "end", attemptId: this.attemptId, revision: this.nextRevision(), index: this.index, outcome: { kind: "abandoned" } });
37
+ }
38
+ }
39
+ const TRACE = process.env.AGY_TRACE === "1";
40
+ const trace = (...parts) => {
41
+ if (TRACE)
42
+ process.stderr.write(`[agy-agent ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
43
+ };
44
+ /** Classify a bridge turn failure into the stable {message, code} UI chip. */
45
+ export function classifyTurnError(error) {
46
+ const text = error instanceof Error ? error.message : String(error);
47
+ if (/429|prepayment|quota|rate limit/i.test(text))
48
+ return { message: text, code: "QUOTA" };
49
+ if (/api key|authentication|unauthenticated|API_KEY/i.test(text))
50
+ return { message: text, code: "AUTH" };
51
+ if (/bridge exited/.test(text))
52
+ return { message: text, code: "BRIDGE_CRASH" };
53
+ return { message: text, code: "UNKNOWN" };
54
+ }
55
+ /** Join the visible text blocks of a Dash user message into the prompt string. */
56
+ export function userMessageText(message) {
57
+ const parts = [];
58
+ for (const block of message.content) {
59
+ if (block.type === "text")
60
+ parts.push(block.text);
61
+ }
62
+ return parts.join("\n");
63
+ }
64
+ /** Whether a raw user text looks like a pasteable Gemini API key. */
65
+ export function looksLikeApiKey(text) {
66
+ const trimmed = text.trim();
67
+ return trimmed.length > 20 && !/\s/.test(trimmed);
68
+ }
69
+ const AGY_TURN_DEADLINE_MS = (() => {
70
+ const raw = process.env.AGY_TURN_DEADLINE_MS;
71
+ if (raw === undefined || raw.trim() === "")
72
+ return 600_000;
73
+ const value = Number(raw);
74
+ return Number.isFinite(value) && value >= 0 ? value : 600_000;
75
+ })();
76
+ const ONBOARDING_PROMPT = "未检测到 Gemini API key。请粘贴你的 Gemini API key(在 https://aistudio.google.com/apikey 免费获取),我会保存后开始会话。";
77
+ export class AgyAgent {
78
+ id;
79
+ options;
80
+ session;
81
+ inbox;
82
+ ctx;
83
+ client;
84
+ loopCtx;
85
+ scope;
86
+ dispatch;
87
+ runtimeInfo;
88
+ streaming = false;
89
+ dashTurn = 0;
90
+ step = 0;
91
+ lastTurn = 0;
92
+ turnOpen = false;
93
+ cancelCause = null;
94
+ streamBridge;
95
+ assistantAttemptCounter = 0;
96
+ assistantStreamRevision = 0;
97
+ remoteQueue = [];
98
+ turnDeadlineTimer = undefined;
99
+ disposed = false;
100
+ systemMessageDone = false;
101
+ activityDone = Promise.resolve();
102
+ resolveActivityDone = () => { };
103
+ onIdleExit;
104
+ constructor(loopCtx, id, options, session, client, onIdleExit, runtimeInfo) {
105
+ this.loopCtx = loopCtx;
106
+ this.id = id;
107
+ this.options = options;
108
+ this.session = withSystemHead(session, () => createSystemMessage(""));
109
+ this.client = client;
110
+ this.onIdleExit = onIdleExit;
111
+ this.runtimeInfo = runtimeInfo;
112
+ this.dispatch = agentEvents(loopCtx, this);
113
+ this.inbox = new Inbox();
114
+ this.scope = createScope(loopCtx, this);
115
+ this.ctx = this.scope.ctx.extend({ agent: this });
116
+ this.lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
117
+ // Thinking-delta sink: the client forwards Thought deltas here; the turn
118
+ // loop buffers them into the assistant reasoning block.
119
+ this.client.onThinkingDelta = (text) => {
120
+ this.pendingThinking.push(text);
121
+ };
122
+ // Tool lifecycle sink: tool_call closes the assistant step and opens a
123
+ // step-scoped tool/call; tool_result appends the paired result (§11).
124
+ this.client.onToolEvent = (ev) => {
125
+ if (!this.turnOpen)
126
+ return;
127
+ if (ev.event === "tool_call") {
128
+ if (this.step > 0)
129
+ this.closeStep();
130
+ this.commitStepStart();
131
+ const callId = ToolCallId(ev.id || `agy-${this.dashTurn}-${this.step}`);
132
+ this.session.append("tool/call", {
133
+ turn: this.dashTurn, step: this.step,
134
+ callId, name: ev.name, arguments: ev.args || "{}",
135
+ });
136
+ this.unpairedCalls.push({ callId, name: ev.name });
137
+ }
138
+ else {
139
+ // ToolResult chunks: the SDK yields none today; if a future version
140
+ // does, pair them off immediately so no placeholder is written.
141
+ this.unpairedCalls = this.unpairedCalls.filter((c) => c.callId !== (ev.id || ""));
142
+ this.session.append("tool/result", {
143
+ turn: this.dashTurn, step: this.step,
144
+ message: createToolResultMessage({
145
+ callId: ToolCallId(ev.id || "unknown"),
146
+ content: [{ type: "text", text: ev.result || "" }],
147
+ isError: ev.is_error,
148
+ }),
149
+ ...(ev.is_error ? { error: { name: "ToolExecutionError", code: "TOOL_ERROR" } } : {}),
150
+ }, { surfaceOp: "append" });
151
+ }
152
+ };
153
+ // Ask-mode tool approvals (hermes #handleApproval shape): the client only
154
+ // consults this when its start frame said approval_mode="ask"; the handler
155
+ // fails closed (no service / error / timeout → deny).
156
+ this.client.setApprovalHandler(async (req) => {
157
+ const approval = this.loopCtx.get("approval");
158
+ if (approval === undefined) {
159
+ trace("approval: no approval service — deny (fail closed)");
160
+ return false;
161
+ }
162
+ const controller = new AbortController();
163
+ try {
164
+ const outcome = await raceApproval(approval.request({
165
+ agent: this,
166
+ toolName: req.tool,
167
+ reason: `Antigravity tool call: ${req.args.slice(0, 200)}`,
168
+ signal: controller.signal,
169
+ }), controller, AGY_APPROVAL_TIMEOUT_MS);
170
+ // Dash approval outcomes: "allowed-once" / "allowed-always" → allow;
171
+ // everything else (denied / dismissed / unknown) → deny.
172
+ return outcome === "allowed-once" || outcome === "allowed-always";
173
+ }
174
+ catch {
175
+ trace(`approval: ${req.tool} request failed — deny (fail closed)`);
176
+ return false;
177
+ }
178
+ });
179
+ }
180
+ get status() {
181
+ return this.streaming ? "running" : "idle";
182
+ }
183
+ send(message, _target, wakeup) {
184
+ this.deliver(message, wakeup);
185
+ }
186
+ followup(message) {
187
+ this.deliver(message, true);
188
+ }
189
+ /** No native steer surface on the bridge (V1 gap): degrade to a parked follow-up. */
190
+ steer(message) {
191
+ this.remoteQueue.push({ message, sent: false });
192
+ }
193
+ inject(message) {
194
+ this.remoteQueue.push({ message, sent: false });
195
+ }
196
+ /** The bridge has no in-turn interrupt (V1 gap): record the cause; applied at the terminal. */
197
+ cancel(cause, _options) {
198
+ if (!this.turnOpen)
199
+ return;
200
+ if (this.cancelCause === null)
201
+ this.cancelCause = cause;
202
+ }
203
+ async whenIdle() {
204
+ let activity;
205
+ do {
206
+ activity = this.activityDone;
207
+ await activity;
208
+ } while (activity !== this.activityDone);
209
+ }
210
+ runMaintenance(task) {
211
+ return task(new AbortController().signal);
212
+ }
213
+ async dispose() {
214
+ if (this.disposed)
215
+ return;
216
+ this.disposed = true;
217
+ this.clearTurnDeadline();
218
+ await this.client.close();
219
+ await this.scope.dispose();
220
+ }
221
+ // ── internals ────────────────────────────────────────────────────────────
222
+ deliver(message, wakeup) {
223
+ if (this.disposed)
224
+ return;
225
+ if (this.turnOpen || this.streaming || !wakeup) {
226
+ this.remoteQueue.push({ message, sent: false });
227
+ return;
228
+ }
229
+ this.deliverPrompt(message);
230
+ }
231
+ deliverPrompt(message) {
232
+ if (this.disposed)
233
+ return;
234
+ this.reserveTurn();
235
+ this.beginActivity();
236
+ void this.startTurn(message);
237
+ }
238
+ sessionIdentityCommitted = false;
239
+ /** Stamp the single-preset selection once per session (projection seed). */
240
+ bootstrapSessionIdentity() {
241
+ if (this.sessionIdentityCommitted)
242
+ return;
243
+ this.sessionIdentityCommitted = true;
244
+ if (!this.session.snapshotEvents().some((event) => event.type === "agent-preset/selected" && event.data?.agentPreset === "agy")) {
245
+ this.session.append("agent-preset/selected", { agentPreset: "agy" });
246
+ }
247
+ }
248
+ async startTurn(message) {
249
+ this.bootstrapSessionIdentity();
250
+ this.session.append("turn/start", { turn: this.dashTurn });
251
+ this.session.append("user/message", message, { surfaceOp: "append" });
252
+ const text = userMessageText(message);
253
+ // §17 onboarding (minimal): no key → answer locally, never spawn the bridge.
254
+ if (this.runtimeInfo?.awaitingKey === true) {
255
+ this.commitStepStart();
256
+ if (looksLikeApiKey(text)) {
257
+ this.runtimeInfo.onSaveApiKey(text.trim());
258
+ this.appendSystemNotice("Gemini API key 已保存到本会话配置,现在开始。");
259
+ trace("onboarding: key stored");
260
+ }
261
+ else {
262
+ this.appendSystemNotice(ONBOARDING_PROMPT);
263
+ }
264
+ this.closeTurn({ kind: "completed" });
265
+ this.markIdle();
266
+ this.endActivity();
267
+ return;
268
+ }
269
+ this.armTurnDeadline();
270
+ this.pendingThinking = [];
271
+ try {
272
+ let full = "";
273
+ for await (const chunk of this.client.turn(text)) {
274
+ this.markRunning();
275
+ this.ensureStreamBridge().push({ type: "text-delta", index: full.length, text: chunk });
276
+ full += chunk;
277
+ }
278
+ this.clearTurnDeadline();
279
+ // SDK 0.1.17 streams ToolCall but NEVER ToolResult (verified live and
280
+ // in SDK source: receive_chunks yields Thought/Text/ToolCall only; the
281
+ // result is fed to localharness via InputEvent and retained nowhere
282
+ // public). Pair EVERY unpaired call with the truthful placeholder so
283
+ // the log stays invariant-clean without fabricating outcomes — else
284
+ // resume closers stamp them all "Interrupted".
285
+ for (const call of this.unpairedCalls) {
286
+ this.session.append("tool/result", {
287
+ turn: this.dashTurn, step: this.step,
288
+ message: createToolResultMessage({
289
+ callId: ToolCallId(call.callId),
290
+ content: [{ type: "text", text: "[the Antigravity SDK does not expose tool execution results]" }],
291
+ isError: false,
292
+ }),
293
+ }, { surfaceOp: "append" });
294
+ }
295
+ this.unpairedCalls = [];
296
+ const reasoning = this.pendingThinking.join("");
297
+ this.pendingThinking = [];
298
+ const usage = convertAgyUsage(this.client.lastUsage);
299
+ if (this.step === 0)
300
+ this.commitStepStart(); // no chunks streamed: open the step for the bare message
301
+ this.appendAssistantMessage(full, { reasoning, usage });
302
+ this.closeStep();
303
+ this.closeTurn(this.cancelCause !== null ? { kind: "aborted", reason: this.cancelCause } : { kind: "completed" });
304
+ this.cancelCause = null;
305
+ trace(`turn ${this.dashTurn} done (${full.length} chars, conversation=${this.client.conversationId})`);
306
+ this.runtimeInfo?.onTurnDone?.();
307
+ this.markIdle();
308
+ this.endActivity();
309
+ this.flushFollowUps();
310
+ }
311
+ catch (error) {
312
+ this.clearTurnDeadline();
313
+ this.abandonStreamBridge();
314
+ const failure = classifyTurnError(error);
315
+ trace(`turn failed [${failure.code}]: ${failure.message.slice(0, 200)}`);
316
+ for (const call of this.unpairedCalls) {
317
+ this.session.append("tool/result", {
318
+ turn: this.dashTurn, step: this.step,
319
+ message: createToolResultMessage({
320
+ callId: ToolCallId(call.callId),
321
+ content: [{ type: "text", text: "[tool result unavailable: the turn failed]" }],
322
+ isError: true,
323
+ }),
324
+ }, { surfaceOp: "append" });
325
+ }
326
+ this.unpairedCalls = [];
327
+ if (this.turnOpen && this.step > 0)
328
+ this.closeStep();
329
+ this.closeTurn({ kind: "error", error: failure });
330
+ this.markIdle();
331
+ this.endActivity();
332
+ }
333
+ }
334
+ appendSystemNotice(text) {
335
+ if (!this.turnOpen)
336
+ return;
337
+ this.session.append("system/message", { turn: this.dashTurn, step: this.step, message: createSystemMessage(text) }, { surfaceOp: "append" });
338
+ }
339
+ appendAssistantMessage(text, extras) {
340
+ const content = [];
341
+ if (extras?.reasoning)
342
+ content.push({ type: "reasoning", text: extras.reasoning });
343
+ content.push({ type: "text", text });
344
+ const assistant = createAssistantMessage({
345
+ content,
346
+ source: {
347
+ provider: this.options.provider ?? "agy",
348
+ model: this.options.model ?? "",
349
+ },
350
+ });
351
+ const bridge = this.streamBridge;
352
+ const event = this.session.append("assistant/message", {
353
+ turn: this.dashTurn,
354
+ step: this.step,
355
+ message: assistant,
356
+ stream: bridge?.records ?? [],
357
+ // usage: the bridge emits it best-effort after the stream — when it is
358
+ // missing there is NO usage field (never fabricated).
359
+ ...(extras?.usage === undefined ? {} : { usage: extras.usage }),
360
+ }, { surfaceOp: "append" });
361
+ if (bridge !== undefined && !bridge.ended)
362
+ bridge.settle(event.seq);
363
+ this.streamBridge = undefined;
364
+ }
365
+ ensureStreamBridge() {
366
+ if (this.streamBridge === undefined || this.streamBridge.ended) {
367
+ this.streamBridge = new AssistantStreamBridge(this.session.id, ++this.assistantAttemptCounter, () => ++this.assistantStreamRevision, (frame) => this.dispatch.emit("agent/assistant-stream", { frame }));
368
+ this.commitStepStart();
369
+ this.streamBridge.start(this.dashTurn, this.step);
370
+ }
371
+ return this.streamBridge;
372
+ }
373
+ abandonStreamBridge() {
374
+ if (this.streamBridge !== undefined && !this.streamBridge.ended)
375
+ this.streamBridge.abandon();
376
+ this.streamBridge = undefined;
377
+ }
378
+ reserveTurn() {
379
+ if (this.turnOpen)
380
+ return;
381
+ this.dashTurn = ++this.lastTurn;
382
+ this.step = 0;
383
+ this.turnOpen = true;
384
+ this.markRunning();
385
+ }
386
+ closeTurn(reason) {
387
+ if (!this.turnOpen)
388
+ return;
389
+ this.turnOpen = false;
390
+ this.clearTurnDeadline();
391
+ this.session.append("turn/end", { turn: this.dashTurn, reason });
392
+ }
393
+ closeStep() {
394
+ if (!this.turnOpen || this.step <= 0)
395
+ return;
396
+ this.session.append("step/end", { turn: this.dashTurn, step: this.step });
397
+ }
398
+ commitStepStart() {
399
+ if (!this.turnOpen)
400
+ return;
401
+ this.step += 1;
402
+ this.session.append("step/start", { turn: this.dashTurn, step: this.step });
403
+ }
404
+ /** One-shot system stamp: the onboarding notice rides the first turn. */
405
+ markRunning() {
406
+ if (this.streaming)
407
+ return;
408
+ this.streaming = true;
409
+ this.dispatch.emit("agent/status", { status: "running" });
410
+ }
411
+ markIdle() {
412
+ if (!this.streaming)
413
+ return;
414
+ this.streaming = false;
415
+ this.dispatch.emit("agent/status", { status: "idle" });
416
+ }
417
+ beginActivity() {
418
+ this.resolveActivityDone();
419
+ this.activityDone = new Promise((resolve) => {
420
+ this.resolveActivityDone = resolve;
421
+ });
422
+ }
423
+ endActivity() {
424
+ this.resolveActivityDone();
425
+ }
426
+ /** Flush ONE parked follow-up as a fresh prompt (never submit while busy). */
427
+ flushFollowUps() {
428
+ if (this.disposed || this.turnOpen || this.streaming)
429
+ return;
430
+ const entry = this.remoteQueue.find((candidate) => !candidate.sent);
431
+ if (entry === undefined)
432
+ return;
433
+ entry.sent = true;
434
+ this.deliverPrompt(entry.message);
435
+ }
436
+ pendingThinking = [];
437
+ /** Calls from this turn still lacking a result (SDK 0.1.17 exposes none). */
438
+ unpairedCalls = [];
439
+ armTurnDeadline() {
440
+ this.clearTurnDeadline();
441
+ if (AGY_TURN_DEADLINE_MS <= 0)
442
+ return;
443
+ this.turnDeadlineTimer = setTimeout(() => {
444
+ if (!this.turnOpen)
445
+ return;
446
+ trace(`turn deadline exceeded after ${AGY_TURN_DEADLINE_MS}ms — failing open turn`);
447
+ this.abandonStreamBridge();
448
+ if (this.step > 0)
449
+ this.closeStep();
450
+ this.closeTurn({ kind: "error", error: { message: `agy turn deadline exceeded after ${AGY_TURN_DEADLINE_MS}ms`, code: "UNKNOWN" } });
451
+ this.markIdle();
452
+ this.endActivity();
453
+ }, AGY_TURN_DEADLINE_MS);
454
+ this.turnDeadlineTimer.unref?.();
455
+ }
456
+ clearTurnDeadline() {
457
+ if (this.turnDeadlineTimer !== undefined) {
458
+ clearTimeout(this.turnDeadlineTimer);
459
+ this.turnDeadlineTimer = undefined;
460
+ }
461
+ }
462
+ }
463
+ /** Approval deadline for one bridge approval round-trip (fail closed). */
464
+ const AGY_APPROVAL_TIMEOUT_MS = 30_000;
465
+ /** Race a promise against its deadline (hermes raceApproval shape). */
466
+ function raceApproval(promise, controller, timeoutMs) {
467
+ return new Promise((resolve, reject) => {
468
+ const timer = setTimeout(() => {
469
+ controller.abort();
470
+ reject(new Error(`approval timed out after ${timeoutMs}ms`));
471
+ }, timeoutMs);
472
+ promise.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });
473
+ });
474
+ }
475
+ /**
476
+ * Map the bridge's best-effort usage event into Dash `TokenUsage`. The bridge
477
+ * derives it from `conversation.last_turn_usage` AFTER the stream — when it is
478
+ * absent there is NO usage field (never fabricated). `thinking_tokens` rides
479
+ * along only when it is a subset of `output_tokens` (the DSH fold rejects a
480
+ * larger value outright).
481
+ */
482
+ export function convertAgyUsage(usage) {
483
+ if (usage === undefined)
484
+ return undefined;
485
+ const inputTokens = Number(usage.input_tokens) || 0;
486
+ const outputTokens = Number(usage.output_tokens) || 0;
487
+ if (inputTokens === 0 && outputTokens === 0)
488
+ return undefined;
489
+ const result = { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
490
+ const thinking = Number(usage.thinking_tokens) || 0;
491
+ if (thinking > 0 && thinking <= outputTokens)
492
+ result.reasoningTokens = thinking;
493
+ return result;
494
+ }