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