@intentic/sandbox-contract 1.288.0 → 1.291.0
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/README.md +4 -1
- package/dist/contracts/agent.contract.d.ts +54 -12
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +14 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +4 -4
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/diff.contract.d.ts +49 -0
- package/dist/contracts/diff.contract.d.ts.map +1 -0
- package/dist/contracts/diff.contract.js +14 -0
- package/dist/contracts/diff.contract.js.map +1 -0
- package/dist/contracts/extensions.contract.d.ts +1 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +95 -95
- package/dist/contracts/sessions.contract.d.ts +13 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +58 -30
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.d.ts +15 -0
- package/dist/contracts/usage.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.js +12 -2
- package/dist/contracts/usage.contract.js.map +1 -1
- package/dist/events/agent-events.d.ts +39 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +95 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +16 -2
- package/dist/events/transcript.js.map +1 -1
- package/dist/events/verify-nudge.d.ts +4 -0
- package/dist/events/verify-nudge.d.ts.map +1 -0
- package/dist/events/verify-nudge.js +4 -0
- package/dist/events/verify-nudge.js.map +1 -0
- package/dist/events/watch-wake.d.ts +14 -0
- package/dist/events/watch-wake.d.ts.map +1 -0
- package/dist/events/watch-wake.js +42 -0
- package/dist/events/watch-wake.js.map +1 -0
- package/dist/index.d.ts +168 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/models/agent-catalog.d.ts +1 -0
- package/dist/models/agent-catalog.d.ts.map +1 -1
- package/dist/models/agent-catalog.js +1 -0
- package/dist/models/agent-catalog.js.map +1 -1
- package/dist/models/agent-runtimes.d.ts.map +1 -1
- package/dist/models/agent-runtimes.js +2 -2
- package/dist/models/agent-runtimes.js.map +1 -1
- package/dist/models/plan-pools.d.ts +13 -0
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +43 -0
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/schemas/agents.d.ts +4 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +3 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/devices.d.ts +21 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +7 -1
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/diff.d.ts +62 -0
- package/dist/schemas/diff.d.ts.map +1 -0
- package/dist/schemas/diff.js +59 -0
- package/dist/schemas/diff.js.map +1 -0
- package/dist/schemas/extension-updates.d.ts +2 -0
- package/dist/schemas/extension-updates.d.ts.map +1 -1
- package/dist/schemas/terminal.d.ts +22 -0
- package/dist/schemas/terminal.d.ts.map +1 -1
- package/dist/schemas/terminal.js +9 -0
- package/dist/schemas/terminal.js.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +1 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +1 -0
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +17 -10
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +5 -5
- package/src/contracts/agents.contract.ts +4 -3
- package/src/contracts/diff.contract.ts +17 -0
- package/src/contracts/usage.contract.ts +19 -2
- package/src/events/transcript.ts +25 -2
- package/src/events/verify-nudge.ts +14 -0
- package/src/events/watch-wake.test.ts +67 -0
- package/src/events/watch-wake.ts +78 -0
- package/src/index.ts +7 -1
- package/src/models/agent-catalog.test.ts +6 -2
- package/src/models/agent-catalog.ts +5 -0
- package/src/models/agent-runtimes.ts +7 -4
- package/src/models/plan-pools.test.ts +40 -1
- package/src/models/plan-pools.ts +69 -0
- package/src/schemas/agents.ts +6 -0
- package/src/schemas/devices.ts +20 -1
- package/src/schemas/diff.ts +72 -0
- package/src/schemas/terminal.ts +11 -0
- package/src/state/history-state.ts +3 -0
- package/src/text/transcript-fold.test.ts +21 -0
- package/src/text/transcript-fold.ts +23 -11
|
@@ -8,6 +8,23 @@ export const RefreshPlanLimitsSchema = z.object({
|
|
|
8
8
|
force: z.boolean().default(false).describe("Measure again even if a reading was taken a moment ago."),
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
// What a re-measure could not read. A provider rate-limits these reads per account, and while it is holding one off
|
|
12
|
+
// the number on screen cannot move: without this the caller can only report a press that changed nothing.
|
|
13
|
+
export const PlanLimitsRefreshedSchema = z.object({
|
|
14
|
+
ok: z.literal(true),
|
|
15
|
+
held: z
|
|
16
|
+
.array(
|
|
17
|
+
z.object({
|
|
18
|
+
provider: z.string().describe("Which provider is holding the read off."),
|
|
19
|
+
account: z.string().describe("The account as its provider's list names it: an account id, or a routed auth file's name."),
|
|
20
|
+
resumesAt: z.number().describe("Unix seconds: when this account may be read again, the provider's own retry-after."),
|
|
21
|
+
}),
|
|
22
|
+
)
|
|
23
|
+
.describe("Accounts whose plan limits could not be read now because the provider is rate-limiting them."),
|
|
24
|
+
});
|
|
25
|
+
export type PlanLimitsRefreshed = z.infer<typeof PlanLimitsRefreshedSchema>;
|
|
26
|
+
export type PlanLimitsHeld = PlanLimitsRefreshed["held"][number];
|
|
27
|
+
|
|
11
28
|
// Durable spend ledger, read-only over the wire; rows are appended daemon-side at turn end.
|
|
12
29
|
// `rollup` groups by day, provider, account and model, so every cost panel re-projects from this one answer.
|
|
13
30
|
export const usageContract = {
|
|
@@ -28,10 +45,10 @@ export const usageContract = {
|
|
|
28
45
|
path: "/usage/plan-limits/refresh",
|
|
29
46
|
summary: "Measure every account's plan limits again",
|
|
30
47
|
description:
|
|
31
|
-
"Reads how full each connected account's plan limits are, for every provider, and records it. Forced, it measures even accounts read a moment ago, which is the right thing when a plan was just changed and the question is whether the number on screen is still true.",
|
|
48
|
+
"Reads how full each connected account's plan limits are, for every provider, and records it. Forced, it measures even accounts read a moment ago, which is the right thing when a plan was just changed and the question is whether the number on screen is still true. Answers with the accounts it could not read because the provider is rate-limiting them, and when each may be asked again: those keep the reading they already had, so a number that does not move is explained rather than silent.",
|
|
32
49
|
})
|
|
33
50
|
.input(RefreshPlanLimitsSchema)
|
|
34
|
-
.output(
|
|
51
|
+
.output(PlanLimitsRefreshedSchema),
|
|
35
52
|
// Asked, not polled: the provider only evaluates this when told the account is at the wall, and the answer isn't
|
|
36
53
|
// cached.
|
|
37
54
|
// Answers `available: false` rather than failing when the account has no such mechanism.
|
package/src/events/transcript.ts
CHANGED
|
@@ -120,6 +120,23 @@ export interface TranscriptTool {
|
|
|
120
120
|
subagent?: TranscriptSubagent | undefined;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Three endings that wake a conversation off a condition watch: the first two are promised when it is armed, the third
|
|
124
|
+
// is the daemon's own (a deadline that passed while it was down). Composing and parsing the wake is watch-wake.ts.
|
|
125
|
+
export const WatchOutcomeSchema = z.enum(["met", "timeout", "restart-expired"]);
|
|
126
|
+
export type WatchOutcome = z.infer<typeof WatchOutcomeSchema>;
|
|
127
|
+
|
|
128
|
+
// A condition watch waking the conversation, as the row carries it. The wake arrives as an ordinary turn prompt that
|
|
129
|
+
// nobody typed, so the row keeps that prompt verbatim rather than a summary of it.
|
|
130
|
+
export const TranscriptWatchWakeSchema = z.object({
|
|
131
|
+
outcome: WatchOutcomeSchema.describe("How the watch ended: the condition held, the deadline passed, or a restart cut it short."),
|
|
132
|
+
note: z.string().describe("The agent's own line on what it was waiting for."),
|
|
133
|
+
elapsed: z
|
|
134
|
+
.string()
|
|
135
|
+
.describe("How long the watch stood, already worded ('43m'): carried rather than recomputed, since the arming instant is not on the row."),
|
|
136
|
+
sent: z.string().describe("The whole prompt the model was woken with, disclosed under the row."),
|
|
137
|
+
});
|
|
138
|
+
export type TranscriptWatchWake = z.infer<typeof TranscriptWatchWakeSchema>;
|
|
139
|
+
|
|
123
140
|
// One note the daemon put before a user's message: the model reads `text`, the chat draws `title` on a row that opens
|
|
124
141
|
// to it. Shared by the live frame and the restored transcript, so it reads the same either way.
|
|
125
142
|
export const TurnNoteSchema = z.object({
|
|
@@ -191,14 +208,19 @@ export const TranscriptRowSchema = z.object({
|
|
|
191
208
|
),
|
|
192
209
|
// The one-press follow-up this notice offers, by name; the chat decides what it does, and whether it stands.
|
|
193
210
|
noticeAction: z
|
|
194
|
-
.enum(["landHold", "outageOptOut", "depsInstall", "tierHold"])
|
|
211
|
+
.enum(["landHold", "outageOptOut", "depsInstall", "tierHold", "watchStop"])
|
|
195
212
|
.optional()
|
|
196
213
|
.describe("A one-press follow-up this notice offers, by name. The chat decides what it does and whether it still applies."),
|
|
197
214
|
// An unfinished wait this notice describes, by name; whether it's still running is live state, not stored here.
|
|
198
215
|
noticeWait: z
|
|
199
|
-
.enum(["credentialRenewal", "personaRoute"])
|
|
216
|
+
.enum(["credentialRenewal", "personaRoute", "watch"])
|
|
200
217
|
.optional()
|
|
201
218
|
.describe("The wait this notice describes, by name, so a reader can say whether it is still on."),
|
|
219
|
+
// Which one, for a wait whose kind can have several in flight at once; without it two armed watches settle together.
|
|
220
|
+
noticeWaitId: z
|
|
221
|
+
.string()
|
|
222
|
+
.optional()
|
|
223
|
+
.describe("Which instance of the wait this notice names, for a kind that can have several running at once."),
|
|
202
224
|
// At most one card per row; a card closes its bubble. One field per kind, so a reader reaches it by name.
|
|
203
225
|
plan: TranscriptPlanSchema.optional().describe("The plan this row asked approval for, and the answer."),
|
|
204
226
|
question: TranscriptQuestionSchema.optional().describe("The questions this row asked, and the picks that answered them."),
|
|
@@ -207,6 +229,7 @@ export const TranscriptRowSchema = z.object({
|
|
|
207
229
|
terminalHelp: TranscriptTerminalHelpSchema.optional().describe("The terminal hand-over this row asked for, and how it ended."),
|
|
208
230
|
capabilityOffer: TranscriptCapabilityOfferSchema.optional().describe("The capability setup this row asked for, the decision, and the outcome."),
|
|
209
231
|
paymentOffer: TranscriptPaymentOfferSchema.optional().describe("The payment this row asked for, the decision, and the receipt."),
|
|
232
|
+
watchWake: TranscriptWatchWakeSchema.optional().describe("The condition watch that woke this conversation, and the prompt it was woken with."),
|
|
210
233
|
credentialOffer: TranscriptCredentialOfferSchema.optional().describe(
|
|
211
234
|
"The gated credential this row asked to use, who may release it, and who did.",
|
|
212
235
|
),
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The follow-up the daemon sends when a turn ended with work it could not confirm: the runtimes with no Stop hook get
|
|
2
|
+
// their turn.ending asks this way (verify-nudge.ts), as an ordinary prompt. Its opening lives on the wire because the
|
|
3
|
+
// chat has to recognise it coming back: a prompt nobody typed must not reach a reader as their own words.
|
|
4
|
+
|
|
5
|
+
// Anchored on by the reader, so it must stay unique and stable across releases — a reworded opening un-recognises every
|
|
6
|
+
// nudge already in a record, and they read as the user's own typing again.
|
|
7
|
+
export const VERIFY_NUDGE_OPENING =
|
|
8
|
+
"The turn you just finished left work this sandbox could not confirm, so it is asking before the work is called done. Each item below is either a check that did not pass, which you repair, or something the turn changed without showing it works, which you prove. Deal with every one of them, then say plainly what you ran and what it covered.";
|
|
9
|
+
|
|
10
|
+
// The prompt a nudge actually sends: its opening, then the findings and asks, each already worded for the model.
|
|
11
|
+
export const verifyNudgePrompt = (parts: readonly string[]): string => [VERIFY_NUDGE_OPENING, ...parts].join("\n\n");
|
|
12
|
+
|
|
13
|
+
// Whether a stored prompt is one, so any reader can ask without checking first.
|
|
14
|
+
export const isVerifyNudge = (prompt: string): boolean => prompt.startsWith(VERIFY_NUDGE_OPENING);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { WatchOutcome } from "./transcript.js";
|
|
3
|
+
import { watchWakeOf, watchWakePrompt, watchWakeRow, type WatchWakeFields } from "./watch-wake.js";
|
|
4
|
+
|
|
5
|
+
const fields = (over: Partial<WatchWakeFields> = {}): WatchWakeFields => ({
|
|
6
|
+
outcome: "met",
|
|
7
|
+
id: "watch-2",
|
|
8
|
+
note: "CI run 316 on intentic/intentic",
|
|
9
|
+
elapsed: "43m",
|
|
10
|
+
command: "gh run view 316 --json status",
|
|
11
|
+
exitCode: 0,
|
|
12
|
+
output: "completed success",
|
|
13
|
+
...over,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const OUTCOMES: readonly WatchOutcome[] = ["met", "timeout", "restart-expired"];
|
|
17
|
+
|
|
18
|
+
describe("watch wake", () => {
|
|
19
|
+
// The composer and the parser are the same piece of knowledge; this is what holds them together when either is
|
|
20
|
+
// reworded.
|
|
21
|
+
it.each(OUTCOMES)("round-trips a %s wake back to its fields", (outcome) => {
|
|
22
|
+
const wake = watchWakeOf(watchWakePrompt(fields({ outcome })));
|
|
23
|
+
expect(wake).toEqual({ outcome, note: "CI run 316 on intentic/intentic", elapsed: "43m", sent: expect.any(String) });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("keeps the prompt verbatim on the row, since nobody typed it", () => {
|
|
27
|
+
const prompt = watchWakePrompt(fields());
|
|
28
|
+
expect(watchWakeOf(prompt)?.sent).toBe(prompt);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("carries the watch id in the body, not the opening", () => {
|
|
32
|
+
const prompt = watchWakePrompt(fields());
|
|
33
|
+
expect(prompt.split("\n")[0]).not.toContain("watch-2");
|
|
34
|
+
expect(prompt).toContain("Watch id: watch-2");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("says an absent exit code in words, since the check never reported one", () => {
|
|
38
|
+
expect(watchWakePrompt(fields({ exitCode: undefined }))).toContain("Last exit code: none (check was killed or failed to start)");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("omits the output block when the check said nothing", () => {
|
|
42
|
+
expect(watchWakePrompt(fields({ output: "" }))).not.toContain("Last output (tail):");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("draws a notice row, never a user row: a watch firing is neither side speaking", () => {
|
|
46
|
+
const row = watchWakeRow(watchWakePrompt(fields()));
|
|
47
|
+
expect(row?.role).toBe("notice");
|
|
48
|
+
expect(row?.text).toBe("CI run 316 on intentic/intentic — the watch fired after 43m.");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("words each ending differently, since each calls for a different next step", () => {
|
|
52
|
+
const textOf = (outcome: WatchOutcome): string => watchWakeRow(watchWakePrompt(fields({ outcome })))?.text ?? "";
|
|
53
|
+
expect(textOf("timeout")).toBe("CI run 316 on intentic/intentic — the watch gave up after 43m.");
|
|
54
|
+
expect(textOf("restart-expired")).toBe("CI run 316 on intentic/intentic — the watch stopped when the sandbox restarted, after 43m.");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("ignores a prompt that is not a wake, so any reader can ask without checking first", () => {
|
|
58
|
+
expect(watchWakeOf("fix the bug")).toBeUndefined();
|
|
59
|
+
expect(watchWakeRow("Watch fired: something I typed myself")).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("refuses a wake whose labelled lines are gone, rather than putting a blank note on the row", () => {
|
|
63
|
+
const prompt = watchWakePrompt(fields());
|
|
64
|
+
expect(watchWakeOf(prompt.replace("Watching: CI run 316 on intentic/intentic\n", ""))).toBeUndefined();
|
|
65
|
+
expect(watchWakeOf(prompt.replace("Elapsed: 43m\n", ""))).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { TranscriptRow, TranscriptWatchWake, WatchOutcome } from "./transcript.js";
|
|
2
|
+
|
|
3
|
+
// What a condition watch says when it wakes a conversation, and how that reads to a person. The wake is delivered as an
|
|
4
|
+
// ordinary turn prompt, so composing it and recognising it must stay one piece of knowledge: three separate readers
|
|
5
|
+
// (the daemon's record, a steered live turn, a provider's session store) turn that prompt back into a row.
|
|
6
|
+
|
|
7
|
+
// Per-outcome opening sentence, which is also what the parser anchors on: keep each unique and stable across releases,
|
|
8
|
+
// or a reworded opening un-recognises wakes already in a record and they read as the user's own words again.
|
|
9
|
+
const OPENINGS: Record<WatchOutcome, string> = {
|
|
10
|
+
met: "Watch fired: the condition you were watching is now met.",
|
|
11
|
+
timeout: "Watch timed out: the deadline passed and the check never exited 0. Decide whether to re-arm it, investigate the check, or report back.",
|
|
12
|
+
"restart-expired":
|
|
13
|
+
"Watch stopped: its deadline passed while the daemon was restarting, so it went unchecked for part of that window. It has just been re-checked once and the condition still does not hold. Decide whether to re-arm it, investigate the check, or report back.",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Labels the prompt writes and the parser reads back. `Watching` and `Elapsed` are load-bearing on both sides; the rest
|
|
17
|
+
// are for the model and the reader only.
|
|
18
|
+
const WATCHING = "Watching: ";
|
|
19
|
+
const ELAPSED = "Elapsed: ";
|
|
20
|
+
|
|
21
|
+
export interface WatchWakeFields {
|
|
22
|
+
readonly outcome: WatchOutcome;
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly note: string;
|
|
25
|
+
readonly elapsed: string;
|
|
26
|
+
readonly command: string;
|
|
27
|
+
// Undefined when the check never exited on its own (killed at its timeout, or failed to spawn).
|
|
28
|
+
readonly exitCode: number | undefined;
|
|
29
|
+
readonly output: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The prompt a wake actually sends. The watch id sits in the body rather than the opening: it is the handle for
|
|
33
|
+
// `watch stop`, so it has to be here, but it is the least useful thing on the page to read first.
|
|
34
|
+
export const watchWakePrompt = (fields: WatchWakeFields): string =>
|
|
35
|
+
[
|
|
36
|
+
OPENINGS[fields.outcome],
|
|
37
|
+
`${WATCHING}${fields.note}`,
|
|
38
|
+
`${ELAPSED}${fields.elapsed}`,
|
|
39
|
+
`Watch id: ${fields.id}`,
|
|
40
|
+
`Check command: ${fields.command}`,
|
|
41
|
+
`Last exit code: ${fields.exitCode === undefined ? "none (check was killed or failed to start)" : fields.exitCode}`,
|
|
42
|
+
...(fields.output === "" ? [] : ["Last output (tail):", "```", fields.output, "```"]),
|
|
43
|
+
"Continue the task this watch was armed for.",
|
|
44
|
+
].join("\n");
|
|
45
|
+
|
|
46
|
+
// The one line a person reads. Present tense for the ending itself, since the row appears the moment it happens.
|
|
47
|
+
const headline = (outcome: WatchOutcome, note: string, elapsed: string): string => {
|
|
48
|
+
if (outcome === "met") {
|
|
49
|
+
return `${note} — the watch fired after ${elapsed}.`;
|
|
50
|
+
}
|
|
51
|
+
if (outcome === "timeout") {
|
|
52
|
+
return `${note} — the watch gave up after ${elapsed}.`;
|
|
53
|
+
}
|
|
54
|
+
return `${note} — the watch stopped when the sandbox restarted, after ${elapsed}.`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const valueOn = (lines: readonly string[], label: string): string | undefined => lines.find((line) => line.startsWith(label))?.slice(label.length);
|
|
58
|
+
|
|
59
|
+
// Which wake a stored prompt is, if any; undefined for every prompt that isn't one, so any reader can ask without
|
|
60
|
+
// checking first. A prompt that opens as a wake but lost its labelled lines is not one: half-parsing it would put a
|
|
61
|
+
// blank note on the row and lose the words with it.
|
|
62
|
+
export const watchWakeOf = (prompt: string): TranscriptWatchWake | undefined => {
|
|
63
|
+
const outcome = (Object.keys(OPENINGS) as WatchOutcome[]).find((key) => prompt.startsWith(OPENINGS[key]));
|
|
64
|
+
if (outcome === undefined) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const lines = prompt.split("\n");
|
|
68
|
+
const note = valueOn(lines, WATCHING);
|
|
69
|
+
const elapsed = valueOn(lines, ELAPSED);
|
|
70
|
+
return note === undefined || elapsed === undefined ? undefined : { outcome, note, elapsed, sent: prompt };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// The row a wake becomes, wherever a prompt is turned into rows: a notice, because a watch firing is something that
|
|
74
|
+
// happened to the turn rather than anything either side said. Undefined when the prompt is not a wake at all.
|
|
75
|
+
export const watchWakeRow = (prompt: string): TranscriptRow | undefined => {
|
|
76
|
+
const wake = watchWakeOf(prompt);
|
|
77
|
+
return wake === undefined ? undefined : { role: "notice", text: headline(wake.outcome, wake.note, wake.elapsed), watchWake: wake };
|
|
78
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { automationsContract } from "./contracts/automations.contract.js";
|
|
|
9
9
|
import { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
10
10
|
import { choresContract } from "./contracts/chores.contract.js";
|
|
11
11
|
import { ciContract } from "./contracts/ci.contract.js";
|
|
12
|
+
import { diffContract } from "./contracts/diff.contract.js";
|
|
12
13
|
import { endpointsContract } from "./contracts/endpoints.contract.js";
|
|
13
14
|
import { exitContract } from "./contracts/exit.contract.js";
|
|
14
15
|
import { extensionsContract } from "./contracts/extensions.contract.js";
|
|
@@ -52,6 +53,7 @@ export { endpointsContract, type TrialHealth, TrialStatusSchema, type TrialStatu
|
|
|
52
53
|
export { exitContract } from "./contracts/exit.contract.js";
|
|
53
54
|
export { extensionsContract } from "./contracts/extensions.contract.js";
|
|
54
55
|
export { personasContract } from "./contracts/personas.contract.js";
|
|
56
|
+
export { diffContract } from "./contracts/diff.contract.js";
|
|
55
57
|
export { gitContract } from "./contracts/git.contract.js";
|
|
56
58
|
export { historyContract } from "./contracts/history.contract.js";
|
|
57
59
|
// Not part of `sandboxContract` below: spoken over a device's WebSocket, with the machine implementing it.
|
|
@@ -81,7 +83,7 @@ export { shareContract } from "./contracts/share.contract.js";
|
|
|
81
83
|
export { skillsContract } from "./contracts/skills.contract.js";
|
|
82
84
|
export { systemContract } from "./contracts/system.contract.js";
|
|
83
85
|
export { translatorContract } from "./contracts/translator.contract.js";
|
|
84
|
-
export { usageContract } from "./contracts/usage.contract.js";
|
|
86
|
+
export { type PlanLimitsHeld, type PlanLimitsRefreshed, PlanLimitsRefreshedSchema, usageContract } from "./contracts/usage.contract.js";
|
|
85
87
|
export { vpnContract } from "./contracts/vpn.contract.js";
|
|
86
88
|
export { workflowsContract } from "./contracts/workflows.contract.js";
|
|
87
89
|
export { workspaceContract } from "./contracts/workspace.contract.js";
|
|
@@ -90,6 +92,8 @@ export * from "./events/cards.js";
|
|
|
90
92
|
export * from "./events/resume.js";
|
|
91
93
|
export * from "./events/system-events.js";
|
|
92
94
|
export * from "./events/transcript.js";
|
|
95
|
+
export * from "./events/verify-nudge.js";
|
|
96
|
+
export * from "./events/watch-wake.js";
|
|
93
97
|
export * from "./policy/card-status.js";
|
|
94
98
|
export * from "./text/mentions.js";
|
|
95
99
|
export * from "./protocol/sse.js";
|
|
@@ -151,6 +155,7 @@ export * from "./schemas/environment.js";
|
|
|
151
155
|
export * from "./schemas/exit.js";
|
|
152
156
|
export * from "./schemas/extension-updates.js";
|
|
153
157
|
export * from "./schemas/providers/fast-mode.js";
|
|
158
|
+
export * from "./schemas/diff.js";
|
|
154
159
|
export * from "./schemas/git/git.js";
|
|
155
160
|
export * from "./schemas/git/git-history.js";
|
|
156
161
|
export * from "./schemas/history.js";
|
|
@@ -209,6 +214,7 @@ export const sandboxContract = {
|
|
|
209
214
|
capabilities: capabilitiesContract,
|
|
210
215
|
chores: choresContract,
|
|
211
216
|
ci: ciContract,
|
|
217
|
+
diff: diffContract,
|
|
212
218
|
endpoints: endpointsContract,
|
|
213
219
|
extensions: extensionsContract,
|
|
214
220
|
personas: personasContract,
|
|
@@ -204,10 +204,14 @@ test("the instruction axis discloses its two weaker answers, differently", () =>
|
|
|
204
204
|
expect(limitationsOf(acpCaps).length).toBeGreaterThan(limitationsOf(codexCaps).length);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
// The backend is one of the daemon's own functions, so it takes a runtime that can call one directly AND a gate the
|
|
208
|
+
// daemon controls: Claude Code has the SDK server plus its PreToolUse matcher, Cursor has customTools plus a consult
|
|
209
|
+
// inside the handler. Every other runtime is a child process with neither, where a script would run unread.
|
|
210
|
+
test("the js execution backend rides the two runtimes that can host the daemon's own tools", () => {
|
|
211
|
+
const hosts: ReadonlySet<string> = new Set(["claude-code", "cursor"]);
|
|
208
212
|
for (const { provider, harness } of pairs) {
|
|
209
213
|
const capabilities = capabilitiesOf(provider, harness);
|
|
210
|
-
expect(capabilities.execution.includes("js")).toBe(capabilities.runtime
|
|
214
|
+
expect(capabilities.execution.includes("js")).toBe(hosts.has(capabilities.runtime));
|
|
211
215
|
}
|
|
212
216
|
});
|
|
213
217
|
|
|
@@ -153,6 +153,11 @@ export const modelsFor = (provider: AgentProvider): CatalogOption[] => {
|
|
|
153
153
|
return [];
|
|
154
154
|
};
|
|
155
155
|
|
|
156
|
+
// Every reasoning tier a catalog row's `efforts` may name, weakest first. One vocabulary for every provider, so the
|
|
157
|
+
// picker can label and order a ladder it has never seen; a provider spelling a rung its own way translates on the way
|
|
158
|
+
// in, at the adapter that read it.
|
|
159
|
+
export const EFFORT_TIERS: readonly string[] = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
160
|
+
|
|
156
161
|
// Filters exactly one pair: Claude's `max` with thinking explicitly false, the one combination Anthropic's API refuses
|
|
157
162
|
// (400). Absent thinking is not off; the model's own default answers, every other tier or provider passes through.
|
|
158
163
|
export const effortAllowed = (effort: string, provider: AgentProvider, thinking: boolean | undefined): boolean =>
|
|
@@ -171,15 +171,17 @@ export const PI: AgentCapabilities = {
|
|
|
171
171
|
// other runtimes lack are function arguments here. The harness axis doesn't apply: the SDK is the only door.
|
|
172
172
|
export const CURSOR: AgentCapabilities = {
|
|
173
173
|
runtime: "cursor",
|
|
174
|
-
//
|
|
175
|
-
steering:
|
|
174
|
+
// Run.steer injects into the live run; only a `complete_delivered` ack transfers ownership of the message.
|
|
175
|
+
steering: true,
|
|
176
176
|
// Cursor's own plan mode, not this repo's emulation; not "modes" since the hook can't gate the whole surface.
|
|
177
177
|
permissions: "plan",
|
|
178
178
|
// True since the daemon supplies its own ask tool; Cursor's own can fabricate an answer, so it's disallowed.
|
|
179
179
|
questions: true,
|
|
180
180
|
// stdio + http/sse MCP servers plus host callbacks; everything but a Claude Code plugin checkout.
|
|
181
181
|
mcp: "tools",
|
|
182
|
-
|
|
182
|
+
// The JS backend rides Cursor's custom-tool seam rather than an MCP server, and consults the rulebook in its
|
|
183
|
+
// handler, since the hook file only covers the shell.
|
|
184
|
+
execution: ["shell", "js"],
|
|
183
185
|
// Cursor publishes effort as model parameters, not one scale; true here just means it's forwardable at all.
|
|
184
186
|
effort: true,
|
|
185
187
|
fastMode: false,
|
|
@@ -191,7 +193,8 @@ export const CURSOR: AgentCapabilities = {
|
|
|
191
193
|
terminals: false,
|
|
192
194
|
// The SDK throws typed errors instead of dissolving a refusal into prose, so the adapter files coded frames.
|
|
193
195
|
recovery: true,
|
|
194
|
-
// append, reached differently: `beforeSubmitPrompt`'s reply folds the
|
|
196
|
+
// append, reached differently: `beforeSubmitPrompt`'s reply folds the daemon's text onto Cursor's base. The SDK can
|
|
197
|
+
// replace that base outright, but only for an entitled account, so the seam every turn can reach is this one.
|
|
195
198
|
instructions: "append",
|
|
196
199
|
skillDiscovery: "prompt",
|
|
197
200
|
// The full hook tier, the only foreign runtime to reach it: a hold parks since the vendor waits on the hook.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "vitest";
|
|
2
|
-
import { bindingWindow, gatesModel, gatingWindows, scopedWindow } from "./plan-pools.js";
|
|
2
|
+
import { bindingWindow, gatesModel, gatingWindows, scopedWindow, windowLive, windowPeriod } from "./plan-pools.js";
|
|
3
3
|
import type { AccountUsage, UsageWindow } from "../schemas/providers/plan-limits.js";
|
|
4
4
|
|
|
5
5
|
// One rule for which pool blocks a given model, shared by the daemon and the browser: a plan can meter models
|
|
@@ -64,3 +64,42 @@ test("names the pool a plan meters this model by on its own, preferring the more
|
|
|
64
64
|
const tied = usage(window({ kind: "model:Opus", gates: { models: ["Opus"] } }), window({ kind: "model:Claude", gates: { models: ["Claude"] } }));
|
|
65
65
|
expect(scopedWindow(tied, { id: "claude-opus-4-6" })).toBeUndefined();
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
// How long a pool's window runs, read off the provider's own key and name. Two things turn on it, which is why it is
|
|
69
|
+
// one function: a narrow column names the window by `short`, and a reading with no published reset may only be trusted
|
|
70
|
+
// for that long.
|
|
71
|
+
|
|
72
|
+
test("a window's length is read off the provider's own key, and off its name where the key says nothing", () => {
|
|
73
|
+
const period = (kind: string, label?: string) => windowPeriod(label === undefined ? { kind } : { kind, label });
|
|
74
|
+
expect(period("five_hour")).toEqual({ seconds: 5 * 3_600, short: "5h" });
|
|
75
|
+
expect(period("seven_day")).toEqual({ seconds: 7 * 86_400, short: "wk" });
|
|
76
|
+
expect(period("seven_day_opus")).toEqual({ seconds: 7 * 86_400, short: "wk" });
|
|
77
|
+
// Anthropic's own key says nothing; the label the reader built from it does.
|
|
78
|
+
expect(period("model:Fable", "Weekly · Fable")).toEqual({ seconds: 7 * 86_400, short: "wk" });
|
|
79
|
+
expect(period("google:gemini-weekly", "Gemini Models · Weekly Limit Remaining")).toEqual({ seconds: 7 * 86_400, short: "wk" });
|
|
80
|
+
expect(period("monthly", "Monthly · all models")).toEqual({ seconds: 30 * 86_400, short: "mo" });
|
|
81
|
+
expect(period("claude:12_hour")).toEqual({ seconds: 12 * 3_600, short: "12h" });
|
|
82
|
+
expect(period("daily")).toEqual({ seconds: 86_400, short: "24h" });
|
|
83
|
+
expect(period("30_minutes")).toEqual({ seconds: 1_800, short: "30m" });
|
|
84
|
+
// A pool nothing names the length of answers nothing, rather than guessing one.
|
|
85
|
+
expect(period("claude:tangelo")).toBeUndefined();
|
|
86
|
+
expect(period("model:Fable", "Fable")).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a reading is live until its reset, or, with none published, for one window's length after it was taken", () => {
|
|
90
|
+
const NOW = 1_700_000_000_000;
|
|
91
|
+
const HOUR = 3_600_000;
|
|
92
|
+
// A published reset is the authority whatever the window's length says, in both directions.
|
|
93
|
+
expect(windowLive(window({ kind: "five_hour", resetsAt: NOW / 1_000 + 60 }), NOW - 50 * HOUR, NOW)).toBe(true);
|
|
94
|
+
expect(windowLive(window({ kind: "seven_day", resetsAt: NOW / 1_000 - 1 }), NOW, NOW)).toBe(false);
|
|
95
|
+
|
|
96
|
+
// With none — what an idle five-hour pool is published with — the window's own length retires it.
|
|
97
|
+
const idle = window({ kind: "five_hour", utilization: 0 });
|
|
98
|
+
expect(windowLive(idle, NOW - 4 * HOUR, NOW)).toBe(true);
|
|
99
|
+
expect(windowLive(idle, NOW - 6 * HOUR, NOW)).toBe(false);
|
|
100
|
+
// A weekly pool read on the same morning is still describing the week it was read in.
|
|
101
|
+
expect(windowLive(window({ kind: "seven_day", utilization: 78 }), NOW - 12 * HOUR, NOW)).toBe(true);
|
|
102
|
+
|
|
103
|
+
// And a pool whose length nothing names has only its reset instant; without one it stands.
|
|
104
|
+
expect(windowLive(window({ kind: "claude:tangelo" }), NOW - 400 * HOUR, NOW)).toBe(true);
|
|
105
|
+
});
|
package/src/models/plan-pools.ts
CHANGED
|
@@ -63,3 +63,72 @@ export const scopedWindow = (usage: AccountUsage | undefined, model: ModelRef):
|
|
|
63
63
|
}
|
|
64
64
|
return best.window;
|
|
65
65
|
};
|
|
66
|
+
|
|
67
|
+
// How long a pool's window runs, read off the provider's own key and whatever name it published. One implementation,
|
|
68
|
+
// because two things need it and must agree: a narrow column names the window by `short`, and a reading with no
|
|
69
|
+
// published reset instant may only be trusted for `seconds` past the moment it was taken.
|
|
70
|
+
|
|
71
|
+
export interface WindowPeriod {
|
|
72
|
+
readonly seconds: number;
|
|
73
|
+
// The token a narrow column names this window by, e.g. "5h", "wk".
|
|
74
|
+
readonly short: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const HOUR_SECONDS = 3_600;
|
|
78
|
+
const DAY_SECONDS = 86_400;
|
|
79
|
+
|
|
80
|
+
/** Kind and label as space-padded lowercase words, underscores split, so `\b` matches across both spellings. */
|
|
81
|
+
export const periodWords = (pool: { readonly kind: string; readonly label?: string | undefined }): string =>
|
|
82
|
+
` ${`${pool.kind} ${pool.label ?? ""}`
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replaceAll(/[^a-z0-9]+/gu, " ")
|
|
85
|
+
.trim()} `;
|
|
86
|
+
|
|
87
|
+
// A count the words spell out, e.g. the 3 in "3 days".
|
|
88
|
+
const counted = (words: string, pattern: RegExp): number | undefined => {
|
|
89
|
+
const found = pattern.exec(words)?.[1];
|
|
90
|
+
return found === undefined ? undefined : Number(found);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Day-scale and longer, where the named periods outrank a bare count: "7 days" is the week every plan sells, not a
|
|
94
|
+
// seven-day span of its own.
|
|
95
|
+
const dayPeriod = (words: string, days: number | undefined): WindowPeriod | undefined => {
|
|
96
|
+
if (days === 7 || /\bseven days?\b|\bweek(ly|s)?\b/u.test(words)) {
|
|
97
|
+
return { seconds: 7 * DAY_SECONDS, short: "wk" };
|
|
98
|
+
}
|
|
99
|
+
if (/\bmonth(ly|s)?\b/u.test(words)) {
|
|
100
|
+
return { seconds: 30 * DAY_SECONDS, short: "mo" };
|
|
101
|
+
}
|
|
102
|
+
if (days === 1 || /\bdaily\b/u.test(words)) {
|
|
103
|
+
return { seconds: DAY_SECONDS, short: "24h" };
|
|
104
|
+
}
|
|
105
|
+
return days === undefined ? undefined : { seconds: days * DAY_SECONDS, short: `${days}d` };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Window length behind a pool; undefined when neither the key nor the name says how long it runs. */
|
|
109
|
+
export const windowPeriod = (pool: { readonly kind: string; readonly label?: string | undefined }): WindowPeriod | undefined => {
|
|
110
|
+
const words = periodWords(pool);
|
|
111
|
+
const hours = counted(words, /\b(\d+) hours?\b/u);
|
|
112
|
+
// Hours first: a pool named both ("5 hours, weekly cap") runs on the shorter clock.
|
|
113
|
+
if (hours !== undefined || /\bfive hours?\b/u.test(words)) {
|
|
114
|
+
const span = hours ?? 5;
|
|
115
|
+
return { seconds: span * HOUR_SECONDS, short: `${span}h` };
|
|
116
|
+
}
|
|
117
|
+
const day = dayPeriod(words, counted(words, /\b(\d+) days?\b/u));
|
|
118
|
+
if (day !== undefined) {
|
|
119
|
+
return day;
|
|
120
|
+
}
|
|
121
|
+
const minutes = counted(words, /\b(\d+) minutes?\b/u);
|
|
122
|
+
return minutes === undefined ? undefined : { seconds: minutes * 60, short: `${minutes}m` };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Whether a reading of this window can still be true. A published reset instant is the authority; with none, the
|
|
126
|
+
// window's own length is, since a pool read as empty says nothing about the window that opened after it. A window whose
|
|
127
|
+
// length nothing names keeps the old rule: only its reset instant can retire it.
|
|
128
|
+
export const windowLive = (window: UsageWindow, measuredAt: number, now: number): boolean => {
|
|
129
|
+
if (window.resetsAt !== undefined) {
|
|
130
|
+
return window.resetsAt * 1000 > now;
|
|
131
|
+
}
|
|
132
|
+
const period = windowPeriod(window);
|
|
133
|
+
return period === undefined || measuredAt + period.seconds * 1000 > now;
|
|
134
|
+
};
|
package/src/schemas/agents.ts
CHANGED
|
@@ -418,6 +418,12 @@ export type AgentWatch = NonNullable<AgentSummary["watches"]>[number];
|
|
|
418
418
|
// needs that type declared first.
|
|
419
419
|
export const AgentIdSchema = z.object({ id: z.string().min(1).describe("Which conversation.") });
|
|
420
420
|
|
|
421
|
+
// Naming no watch disarms every one, which is what a press made about the whole card means; a press made about one
|
|
422
|
+
// watch's own transcript row names it, and leaves the conversation's other watches armed.
|
|
423
|
+
export const AgentStopWatchingSchema = AgentIdSchema.extend({
|
|
424
|
+
watchId: z.string().optional().describe("Which watch to disarm. Absent disarms every watch this conversation is parked on."),
|
|
425
|
+
});
|
|
426
|
+
|
|
421
427
|
// Pages a transcript: a read returns its most recent turns and where they start (`from`); handing that back as `before`
|
|
422
428
|
// asks for the page above. A stale cursor (a rewind, a fork) is clamped, never refused.
|
|
423
429
|
export const AgentTranscriptQuerySchema = AgentIdSchema.extend({
|
package/src/schemas/devices.ts
CHANGED
|
@@ -123,9 +123,14 @@ export type DeviceAgentFlowInput = z.infer<typeof DeviceAgentFlowInputSchema>;
|
|
|
123
123
|
// `sync-clean` removes the build output left in directories the sandbox has deleted, which is what stops those
|
|
124
124
|
// deletions from ever landing. It deletes only content the session already ignores, so it is the one command here whose
|
|
125
125
|
// worst outcome is a rebuild — which is why it is a button and not a turn.
|
|
126
|
+
// `mirror-ignore`/`mirror-unignore` are `mirror-off`'s per-port form, and the reason they exist: a port number that is
|
|
127
|
+
// permanently taken on one machine's localhost has no answer in an all-or-nothing switch, so the choice lives where the
|
|
128
|
+
// conflict does — one port, one device, durable — instead of muting every port the pairing serves.
|
|
126
129
|
export const DeviceCommandSchema = z.enum([
|
|
127
130
|
"mirror-off",
|
|
128
131
|
"mirror-on",
|
|
132
|
+
"mirror-ignore",
|
|
133
|
+
"mirror-unignore",
|
|
129
134
|
"sync-pause",
|
|
130
135
|
"sync-resume",
|
|
131
136
|
"sync-unpair",
|
|
@@ -210,6 +215,8 @@ export const DeviceLocalDirSchema = z
|
|
|
210
215
|
.min(1)
|
|
211
216
|
.max(4096)
|
|
212
217
|
.regex(/^(?:~|\/|[A-Za-z]:[\\/])[^"'`$;|&\n\r]*$/);
|
|
218
|
+
// A TCP port number, the one value a caller supplies that reaches a command line as a number rather than a string.
|
|
219
|
+
export const PortNumberSchema = z.number().int().min(1).max(65535);
|
|
213
220
|
export const DeviceCommandInputSchema = z.object({
|
|
214
221
|
id: z.string().min(1),
|
|
215
222
|
command: DeviceCommandSchema,
|
|
@@ -218,6 +225,9 @@ export const DeviceCommandInputSchema = z.object({
|
|
|
218
225
|
// for file sync, the folder on that device. The pairing token is minted by the daemon; no caller ever carries one.
|
|
219
226
|
mode: z.enum(["sync", "mirror"]).optional(),
|
|
220
227
|
localDir: DeviceLocalDirSchema.optional(),
|
|
228
|
+
// The two per-port mirror switches only. A number rather than a string, so nothing a caller sends can widen the
|
|
229
|
+
// command line it lands in.
|
|
230
|
+
port: PortNumberSchema.optional(),
|
|
221
231
|
});
|
|
222
232
|
export type DeviceCommandInput = z.infer<typeof DeviceCommandInputSchema>;
|
|
223
233
|
// `ok` is the command's own exit status, not this route's: a refusal or non-zero exit is a real answer, not a thrown
|
|
@@ -294,9 +304,18 @@ export const DevicePortStateSchema = z.enum([
|
|
|
294
304
|
"held-by-sandbox",
|
|
295
305
|
// Something outside this product already binds the port; not ours to name or take.
|
|
296
306
|
"busy",
|
|
307
|
+
// The owner told this device to leave this number alone (`sync mirror ignore`); nothing was attempted.
|
|
308
|
+
"ignored",
|
|
297
309
|
]);
|
|
310
|
+
export type DevicePortState = z.infer<typeof DevicePortStateSchema>;
|
|
311
|
+
|
|
312
|
+
// Why a port the sandbox serves is not on this machine's localhost: the state minus its one success. Named here
|
|
313
|
+
// because the machine decides it and the browser renders it, so a new reason has to reach both at once.
|
|
314
|
+
export const PortSkipReasonSchema = DevicePortStateSchema.exclude(["mirrored"]);
|
|
315
|
+
export type PortSkipReason = z.infer<typeof PortSkipReasonSchema>;
|
|
316
|
+
|
|
298
317
|
export const DevicePortSchema = z.object({
|
|
299
|
-
port:
|
|
318
|
+
port: PortNumberSchema,
|
|
300
319
|
host: z.enum(["127.0.0.1", "::1"]),
|
|
301
320
|
// The sandbox serving the port, whose /ports listed it, not whoever ended up holding the local bind.
|
|
302
321
|
sandboxId: z.string(),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// diff: one side of a file diff named by its source, for the routes that answer with something other than the text
|
|
2
|
+
// diff itself (bytes on /diff/raw, derived text on /diff/derived).
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { GitDiffSideSchema } from "./git/git.js";
|
|
5
|
+
|
|
6
|
+
const path = z.string().min(1).describe("The file, relative to the repo or scope the diff belongs to.");
|
|
7
|
+
const repo = z.string().min(1).describe('Which repository: "root" for the workspace itself, otherwise a repo id.');
|
|
8
|
+
|
|
9
|
+
// The four pairings a review surface lists a row under; every field is a string, so the same shape rides a query string
|
|
10
|
+
// and the JSON body of a typed call alike.
|
|
11
|
+
export const DiffSourceQuerySchema = z.discriminatedUnion("source", [
|
|
12
|
+
z
|
|
13
|
+
.object({
|
|
14
|
+
source: z.literal("working"),
|
|
15
|
+
repo,
|
|
16
|
+
side: GitDiffSideSchema.describe("Which git side the row came from; a half-staged file is two different diffs."),
|
|
17
|
+
path,
|
|
18
|
+
})
|
|
19
|
+
.describe("Uncommitted work in a workspace repo, what the Changes panel lists."),
|
|
20
|
+
z
|
|
21
|
+
.object({
|
|
22
|
+
source: z.literal("agent"),
|
|
23
|
+
agent: z.string().min(1).describe("The conversation whose work is under review."),
|
|
24
|
+
repo,
|
|
25
|
+
path,
|
|
26
|
+
})
|
|
27
|
+
.describe("One agent's work against the base its review is listed against."),
|
|
28
|
+
z
|
|
29
|
+
.object({
|
|
30
|
+
source: z.literal("commit"),
|
|
31
|
+
repo,
|
|
32
|
+
sha: z
|
|
33
|
+
.string()
|
|
34
|
+
.regex(/^[0-9a-f]{4,64}$/)
|
|
35
|
+
.describe("The commit, compared against its first parent."),
|
|
36
|
+
path,
|
|
37
|
+
})
|
|
38
|
+
.describe("A commit against its first parent."),
|
|
39
|
+
z
|
|
40
|
+
.object({
|
|
41
|
+
source: z.literal("checkpoint"),
|
|
42
|
+
snapshot: z.string().min(1).describe("Which saved point."),
|
|
43
|
+
scope: z.string().min(1).describe("Which part of the workspace the path belongs to."),
|
|
44
|
+
path,
|
|
45
|
+
})
|
|
46
|
+
.describe("A saved point in the timeline, against the visible one before it."),
|
|
47
|
+
]);
|
|
48
|
+
export type DiffSourceQuery = z.infer<typeof DiffSourceQuerySchema>;
|
|
49
|
+
|
|
50
|
+
// One side of a document rendered to text, or why it could not be. The text is fileq's rendering, the same one an
|
|
51
|
+
// agent reads instead of the bytes, so what a reviewer compares here is what the agent worked from.
|
|
52
|
+
export const DerivedSideSchema = z.discriminatedUnion("present", [
|
|
53
|
+
z.object({
|
|
54
|
+
present: z.literal(true),
|
|
55
|
+
content: z.string().describe("The side as markdown."),
|
|
56
|
+
deriver: z.string().describe("Which reader made this text, with its version."),
|
|
57
|
+
notes: z.array(z.string()).describe("Every cap and degradation the conversion hit, one line each."),
|
|
58
|
+
truncated: z.boolean().describe("The rendering was longer than this response carries; only its start is here."),
|
|
59
|
+
}),
|
|
60
|
+
z.object({
|
|
61
|
+
present: z.literal(false),
|
|
62
|
+
reason: z.string().describe("Why this side has no text: a format nothing reads, a broken file, a sandbox with no reader."),
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
export type DerivedSide = z.infer<typeof DerivedSideSchema>;
|
|
66
|
+
|
|
67
|
+
// Both sides as text; an absent side is a side the diff does not have (an added file has no before).
|
|
68
|
+
export const DerivedDiffSchema = z.object({
|
|
69
|
+
before: DerivedSideSchema.optional().describe("The file as it was, rendered to text. Absent when it did not exist yet."),
|
|
70
|
+
after: DerivedSideSchema.optional().describe("The file as it is now, rendered to text. Absent when it was deleted."),
|
|
71
|
+
});
|
|
72
|
+
export type DerivedDiff = z.infer<typeof DerivedDiffSchema>;
|