@oxygen-agent/cli 1.894.0 → 1.917.5
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 +1 -1
- package/dist/command-manifest.js +11 -3
- package/dist/index.js +348 -43
- package/node_modules/@oxygen/formula/dist/expression.d.ts +21 -0
- package/node_modules/@oxygen/formula/dist/expression.js +42 -1
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +1 -1
- package/node_modules/@oxygen/formula/dist/formula-functions.js +10 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +30 -7
- package/node_modules/@oxygen/shared/dist/copilot-errors.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/copilot-errors.js +9 -0
- package/node_modules/@oxygen/shared/dist/copilot-plan.d.ts +169 -0
- package/node_modules/@oxygen/shared/dist/copilot-plan.js +476 -0
- package/node_modules/@oxygen/shared/dist/dnc-identities.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/dnc-identities.js +23 -0
- package/node_modules/@oxygen/shared/dist/egress-transport-readiness.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/egress-transport-readiness.js +67 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/index.js +3 -0
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +93 -5
- package/node_modules/@oxygen/shared/dist/langfuse.js +326 -42
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +16 -5
- package/node_modules/@oxygen/shared/dist/product-briefing-rules.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/product-briefing-rules.js +291 -0
- package/node_modules/@oxygen/shared/dist/product-doctrine.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/product-doctrine.js +70 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +18 -11
- package/node_modules/@oxygen/shared/dist/sequences.js +47 -13
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +23 -2
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +10 -0
- package/package.json +5 -2
|
@@ -41,6 +41,27 @@ export type ParseOptions = {
|
|
|
41
41
|
export declare function parseFormulaExpression(expression: string, options?: ParseOptions): FormulaAst;
|
|
42
42
|
/** Validate supported functions and arity on an already-parsed tree. */
|
|
43
43
|
export declare function validateFormulaAst(ast: FormulaAst): void;
|
|
44
|
+
/**
|
|
45
|
+
* SAVE-TIME ONLY: reject a literal regex pattern that is too long to ever compile.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately NOT part of validateFormulaAst. That runs on the READ path too
|
|
48
|
+
* (formula-column-runner prepares every batch through it) and
|
|
49
|
+
* prepareFormulaForRead swallows the throw and nulls the WHOLE column, so
|
|
50
|
+
* tightening it would silently blank already-stored columns rather than reject
|
|
51
|
+
* the edit. Keeping this save-only means a column that stores today keeps
|
|
52
|
+
* reading today, and only a NEW write is refused.
|
|
53
|
+
*
|
|
54
|
+
* Length is checked, not compilation. A pattern over the cap can never succeed in
|
|
55
|
+
* any branch, so flagging it costs the author nothing -- whereas rejecting an
|
|
56
|
+
* unparseable literal would break `if(cond, regex_match(x, "["), "n/a")`, which
|
|
57
|
+
* works today precisely because if/switch/and/or/coalesce are lazy and never
|
|
58
|
+
* evaluate the untaken side. That stricter semantic is a separate decision.
|
|
59
|
+
*
|
|
60
|
+
* Without this, an over-long pattern stored fine and then failed per row on every
|
|
61
|
+
* run forever -- the production shape was one column failing across dozens of rows
|
|
62
|
+
* for days with received_length 290 against the old 256 cap.
|
|
63
|
+
*/
|
|
64
|
+
export declare function validateFormulaRegexLiterals(ast: FormulaAst): void;
|
|
44
65
|
/** Validate syntax, supported functions, and arity without evaluating any data. */
|
|
45
66
|
export declare function validateFormulaExpression(expression: string, options?: ParseOptions): void;
|
|
46
67
|
export declare function walkExpression(node: FormulaAst, visit: (node: FormulaAst) => void): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FORMULA_FUNCTION_REGISTRY, checkFormulaFunctionArity, formulaExpressionError, } from "./formula-functions.js";
|
|
1
|
+
import { FORMULA_FUNCTION_REGISTRY, MAX_FORMULA_REGEX_PATTERN_LENGTH, checkFormulaFunctionArity, formulaExpressionError, } from "./formula-functions.js";
|
|
2
2
|
/**
|
|
3
3
|
* Functions evaluated against the whole column rather than the current scope.
|
|
4
4
|
* They are intercepted before registry dispatch (the registry entry carries
|
|
@@ -37,6 +37,47 @@ export function validateFormulaAst(ast) {
|
|
|
37
37
|
checkFormulaFunctionArity(spec, node.args.length);
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* SAVE-TIME ONLY: reject a literal regex pattern that is too long to ever compile.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately NOT part of validateFormulaAst. That runs on the READ path too
|
|
44
|
+
* (formula-column-runner prepares every batch through it) and
|
|
45
|
+
* prepareFormulaForRead swallows the throw and nulls the WHOLE column, so
|
|
46
|
+
* tightening it would silently blank already-stored columns rather than reject
|
|
47
|
+
* the edit. Keeping this save-only means a column that stores today keeps
|
|
48
|
+
* reading today, and only a NEW write is refused.
|
|
49
|
+
*
|
|
50
|
+
* Length is checked, not compilation. A pattern over the cap can never succeed in
|
|
51
|
+
* any branch, so flagging it costs the author nothing -- whereas rejecting an
|
|
52
|
+
* unparseable literal would break `if(cond, regex_match(x, "["), "n/a")`, which
|
|
53
|
+
* works today precisely because if/switch/and/or/coalesce are lazy and never
|
|
54
|
+
* evaluate the untaken side. That stricter semantic is a separate decision.
|
|
55
|
+
*
|
|
56
|
+
* Without this, an over-long pattern stored fine and then failed per row on every
|
|
57
|
+
* run forever -- the production shape was one column failing across dozens of rows
|
|
58
|
+
* for days with received_length 290 against the old 256 cap.
|
|
59
|
+
*/
|
|
60
|
+
export function validateFormulaRegexLiterals(ast) {
|
|
61
|
+
walkExpression(ast, (node) => {
|
|
62
|
+
if (node.type !== "call")
|
|
63
|
+
return;
|
|
64
|
+
const normalized = node.name.toLowerCase();
|
|
65
|
+
if (normalized !== "regex_match" && normalized !== "regex_extract" && normalized !== "regex_replace") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Every regex function takes its pattern as the second argument.
|
|
69
|
+
const pattern = node.args[1];
|
|
70
|
+
if (!pattern || pattern.type !== "literal" || typeof pattern.value !== "string")
|
|
71
|
+
return;
|
|
72
|
+
if (pattern.value.length <= MAX_FORMULA_REGEX_PATTERN_LENGTH)
|
|
73
|
+
return;
|
|
74
|
+
throw formulaExpressionError("Regular-expression pattern is too long.", {
|
|
75
|
+
function: node.name,
|
|
76
|
+
max_length: MAX_FORMULA_REGEX_PATTERN_LENGTH,
|
|
77
|
+
received_length: pattern.value.length,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
40
81
|
/** Validate syntax, supported functions, and arity without evaluating any data. */
|
|
41
82
|
export function validateFormulaExpression(expression, options = {}) {
|
|
42
83
|
validateFormulaAst(parseFormulaExpression(expression, options));
|
|
@@ -37,7 +37,7 @@ export type FormulaFunctionSpec = FormulaFunctionMeta & {
|
|
|
37
37
|
evaluate?: (args: unknown[]) => unknown;
|
|
38
38
|
evaluateLazy?: (thunks: Array<() => unknown>) => unknown;
|
|
39
39
|
};
|
|
40
|
-
export declare const MAX_FORMULA_REGEX_PATTERN_LENGTH =
|
|
40
|
+
export declare const MAX_FORMULA_REGEX_PATTERN_LENGTH = 1000;
|
|
41
41
|
export declare const MAX_FORMULA_REGEX_INPUT_LENGTH = 20000;
|
|
42
42
|
export declare function formulaExpressionError(message: string, details: Record<string, unknown>): OxygenError;
|
|
43
43
|
export declare function isBlankFormulaValue(value: unknown): boolean;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
2
2
|
import { walkJsonPath } from "@oxygen/shared/json-path";
|
|
3
3
|
import { normalizeDomain, normalizeEmail, normalizeLinkedinUrl, } from "./value-normalizers.js";
|
|
4
|
-
|
|
4
|
+
// Pattern LENGTH is not a safety bound and never was -- catastrophic backtracking
|
|
5
|
+
// is a function of pattern SHAPE, and `(a+)+$` hangs at 6 characters, so 256 already
|
|
6
|
+
// permitted an unbounded hang while rejecting harmless long literals. The real bound
|
|
7
|
+
// on scan cost is MAX_FORMULA_REGEX_INPUT_LENGTH below, which caps what a pattern is
|
|
8
|
+
// run against. 256 rejected a customer's 290-character alternation on every row of
|
|
9
|
+
// their table, continuously, for days (production 2026-08-28/31). Raised to a value
|
|
10
|
+
// that still stops a pathological paste while leaving ordinary generated alternations
|
|
11
|
+
// room. If real ReDoS protection is ever needed it belongs in the engine (a
|
|
12
|
+
// backtracking budget or re2), not in a character count.
|
|
13
|
+
export const MAX_FORMULA_REGEX_PATTERN_LENGTH = 1_000;
|
|
5
14
|
export const MAX_FORMULA_REGEX_INPUT_LENGTH = 20_000;
|
|
6
15
|
// ---------------------------------------------------------------------------
|
|
7
16
|
// Shared coercion helpers (used by the registry AND the runner's operators)
|
|
@@ -21,15 +21,20 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
21
21
|
id: "discovery-and-skills",
|
|
22
22
|
layer: "Control",
|
|
23
23
|
primitive: null,
|
|
24
|
-
owns: "Bounded capability, command, provider-operation, Recipe, and product-skill discovery with exact hydration on demand.",
|
|
24
|
+
owns: "Bounded capability, command, provider-operation, Recipe, and product-skill discovery, plus INSTANCE discovery — what this workspace actually holds — with exact hydration on demand.",
|
|
25
25
|
notFor: "Executing GTM work or loading full manifests and schemas before a capability is selected.",
|
|
26
|
-
execution: "Search by outcome, choose the owner, then hydrate one exact command, MCP schema, provider descriptor, or skill.",
|
|
26
|
+
execution: "Search by outcome, choose the owner, then hydrate one exact command, MCP schema, provider descriptor, or skill. For instance discovery, read the map and then the primitive that owns the row.",
|
|
27
27
|
posture: "read_only",
|
|
28
|
-
gatewayTools: ["oxygen_capabilities_search", "oxygen_tools_search", "oxygen_recipes_list"],
|
|
29
|
-
gatewayCommands: ["capabilities search", "commands search", "skills search", "tools search", "recipes list"],
|
|
28
|
+
gatewayTools: ["oxygen_capabilities_search", "oxygen_tools_search", "oxygen_recipes_list", "oxygen_workspace_map"],
|
|
29
|
+
gatewayCommands: ["capabilities search", "commands search", "skills search", "tools search", "recipes list", "workspace map"],
|
|
30
30
|
skills: ["oxygen-quickstart", "oxygen-gtm"],
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// `workspace` (the map) belongs here rather than beside `home`: capability
|
|
32
|
+
// discovery answers what OXYGEN can do and instance discovery answers what THIS
|
|
33
|
+
// workspace holds, and they are the same act — bounded, read-only, hydrate one
|
|
34
|
+
// thing on demand. `home` stayed on the onboarding card because the standup is a
|
|
35
|
+
// first-run surface; the map is not.
|
|
36
|
+
endpointSections: ["skills", "workspace"],
|
|
37
|
+
intentTerms: ["discover", "discovery", "capability", "capabilities", "command", "commands", "skill", "skills", "which tool", "how do i", "what do i have", "what is in my workspace", "workspace map"],
|
|
33
38
|
},
|
|
34
39
|
{
|
|
35
40
|
id: "onboarding-and-copilot",
|
|
@@ -537,6 +542,13 @@ function normalizeIntent(query) {
|
|
|
537
542
|
return query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
538
543
|
}
|
|
539
544
|
function explicitCapabilityIntent(query) {
|
|
545
|
+
// A unified sender profile is an owned Sequence identity, even when the ask
|
|
546
|
+
// names every attached channel (LinkedIn + WhatsApp + email). Resolve this
|
|
547
|
+
// before public LinkedIn research, whose generic "profile" wording would
|
|
548
|
+
// otherwise route an account-readiness question to scraper tools.
|
|
549
|
+
if (isSenderProfileIntent(query)) {
|
|
550
|
+
return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
|
|
551
|
+
}
|
|
540
552
|
if (isInboxAvatarIntent(query)) {
|
|
541
553
|
return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
|
|
542
554
|
}
|
|
@@ -765,6 +777,12 @@ function recommendationsFor(card, query) {
|
|
|
765
777
|
}
|
|
766
778
|
}
|
|
767
779
|
if (card.primitive === "sequences") {
|
|
780
|
+
if (isSenderProfileIntent(query)) {
|
|
781
|
+
return {
|
|
782
|
+
tools: ["oxygen_senders_profiles_list", "oxygen_senders_profiles_get"],
|
|
783
|
+
commands: ["senders profiles list", "senders profiles get"],
|
|
784
|
+
};
|
|
785
|
+
}
|
|
768
786
|
if (isNetNewLinkedInInitiation(query)) {
|
|
769
787
|
const publicResearch = isPublicLinkedInRead(query);
|
|
770
788
|
const harvestEngagers = publicResearch && isPublicLinkedInEngagerHarvest(query);
|
|
@@ -861,7 +879,7 @@ function recommendationsFor(card, query) {
|
|
|
861
879
|
if (card.id === "connected-whatsapp" && /\b(account|connect|limits?|sync)\b/.test(query)) {
|
|
862
880
|
return {
|
|
863
881
|
tools: ["oxygen_whatsapp_accounts_list", "oxygen_whatsapp_get", "oxygen_whatsapp_limits_get", "oxygen_whatsapp_connect"],
|
|
864
|
-
commands: ["whatsapp accounts
|
|
882
|
+
commands: ["whatsapp accounts", "whatsapp get", "whatsapp limits get", "whatsapp connect"],
|
|
865
883
|
};
|
|
866
884
|
}
|
|
867
885
|
return { tools: [...card.gatewayTools], commands: [...card.gatewayCommands] };
|
|
@@ -869,6 +887,11 @@ function recommendationsFor(card, query) {
|
|
|
869
887
|
function isInboxAvatarIntent(query) {
|
|
870
888
|
return /\b(avatar|profile (?:picture|photo)|headshot|hosted (?:picture|image)|mailbox (?:picture|photo))\b/.test(query);
|
|
871
889
|
}
|
|
890
|
+
function isSenderProfileIntent(query) {
|
|
891
|
+
return /\b(?:sender|sending|unified)\s+(?:profiles?|identit(?:y|ies))\b/.test(query)
|
|
892
|
+
|| (/\bprofiles?\b.{0,64}\b(?:linkedin|whatsapp|mailboxes?|inboxes?|email)\b/.test(query)
|
|
893
|
+
&& /\b(?:sender|sending|unif(?:y|ied))\b/.test(query));
|
|
894
|
+
}
|
|
872
895
|
function isMutualLinkedInConnectionsIntent(query) {
|
|
873
896
|
const linkedInContext = /\blinkedin\b|\bprofiles?\b/.test(query);
|
|
874
897
|
const mutualContext = /\b(mutual|shared|in common)\b.{0,48}\b(connections?|relations?)\b/.test(query)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const COPILOT_TURN_TIMEOUT_CODE = "copilot_turn_timeout";
|
|
2
2
|
export declare const COPILOT_TURN_TIMEOUT_MESSAGE = "This Copilot request timed out and stopped. Any completed actions are still saved\u2014review this session before trying again.";
|
|
3
|
+
export declare const COPILOT_TURN_DEADLINE_EXCEEDED_CODE = "copilot_turn_deadline_exceeded";
|
|
3
4
|
export type CustomerFacingCopilotError = {
|
|
4
5
|
code: string | null;
|
|
5
6
|
message: string | null;
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
// and already-persisted failures serialize identically across web, CLI, and MCP.
|
|
5
5
|
export const COPILOT_TURN_TIMEOUT_CODE = "copilot_turn_timeout";
|
|
6
6
|
export const COPILOT_TURN_TIMEOUT_MESSAGE = "This Copilot request timed out and stopped. Any completed actions are still saved—review this session before trying again.";
|
|
7
|
+
// A turn the worker refused to start because it was ALREADY past its stamped
|
|
8
|
+
// wall-clock deadline when it was reclaimed. Distinct in the durable row from
|
|
9
|
+
// `copilot_turn_timeout` (a turn that ran and then ran out of clock) so an
|
|
10
|
+
// operator can separate "we never started it" from "we could not finish it" in
|
|
11
|
+
// SQL — but deliberately NOT distinct to the customer: the mapping below folds
|
|
12
|
+
// it into the same timeout contract every Control surface already renders, per
|
|
13
|
+
// this module's policy.
|
|
14
|
+
export const COPILOT_TURN_DEADLINE_EXCEEDED_CODE = "copilot_turn_deadline_exceeded";
|
|
7
15
|
const WORKER_STEP_TIMEOUT_MESSAGE = /\bWorker step '[^']+' exceeded \d+ms deadline\.?/i;
|
|
8
16
|
/**
|
|
9
17
|
* Replace an internal worker deadline with the stable Copilot timeout contract.
|
|
@@ -15,6 +23,7 @@ export function customerFacingCopilotError(input) {
|
|
|
15
23
|
const message = input.message ?? null;
|
|
16
24
|
const isTimeout = code === "worker_step_timeout" ||
|
|
17
25
|
code === COPILOT_TURN_TIMEOUT_CODE ||
|
|
26
|
+
code === COPILOT_TURN_DEADLINE_EXCEEDED_CODE ||
|
|
18
27
|
(message !== null && WORKER_STEP_TIMEOUT_MESSAGE.test(message));
|
|
19
28
|
return isTimeout
|
|
20
29
|
? { code: COPILOT_TURN_TIMEOUT_CODE, message: COPILOT_TURN_TIMEOUT_MESSAGE }
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four states a plan step may be in.
|
|
3
|
+
*
|
|
4
|
+
* This is the vocabulary `plan_update`'s JSON Schema enforces going forward, and
|
|
5
|
+
* @oxygen/agent-runtime imports it so the schema and this reader can never drift.
|
|
6
|
+
*/
|
|
7
|
+
export declare const COPILOT_PLAN_STEP_STATUSES: readonly ["pending", "in_progress", "done", "blocked"];
|
|
8
|
+
export type CopilotPlanStepStatus = (typeof COPILOT_PLAN_STEP_STATUSES)[number];
|
|
9
|
+
/**
|
|
10
|
+
* An unrecognised status resolves to `pending`, never to `done`.
|
|
11
|
+
*
|
|
12
|
+
* This is the fail-closed direction and it is deliberate. Resolving the unknown
|
|
13
|
+
* to `done` would let a typo report a run as finished and silence the open-steps
|
|
14
|
+
* nudge; resolving it to `pending` at worst leaves a finished step looking open,
|
|
15
|
+
* which is visible and self-correcting on the next plan update.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeCopilotPlanStepStatus(raw: unknown): CopilotPlanStepStatus;
|
|
18
|
+
/** A step is open until it is done. `blocked` is open too -- it still owes an outcome. */
|
|
19
|
+
export declare function isCopilotPlanStepOpen(step: {
|
|
20
|
+
status: CopilotPlanStepStatus;
|
|
21
|
+
}): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The step shape after normalisation, before any timing is attached.
|
|
24
|
+
*
|
|
25
|
+
* `title` accepts three spellings because three have been produced: the tool
|
|
26
|
+
* schema and `packages/copilot`'s dormant `PlanStep` type both say `title`, while
|
|
27
|
+
* the model actually shipped `description` on every step of session 4097ef0d.
|
|
28
|
+
* `parsePlanSteps` in the web app required `title` and `continue`d past anything
|
|
29
|
+
* else, which is why it returned null for every real payload ever written.
|
|
30
|
+
*/
|
|
31
|
+
export type CopilotPlanRawStep = {
|
|
32
|
+
id: string | null;
|
|
33
|
+
title: string;
|
|
34
|
+
status: CopilotPlanStepStatus;
|
|
35
|
+
estimateSeconds: number | null;
|
|
36
|
+
substeps: Array<{
|
|
37
|
+
id: string | null;
|
|
38
|
+
title: string;
|
|
39
|
+
status: CopilotPlanStepStatus;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Pull the step list out of a `plan_updated` payload.
|
|
44
|
+
*
|
|
45
|
+
* Accepts both `{steps}` at the root (what the runtime emits) and the nested
|
|
46
|
+
* `{plan:{steps}}` shape, so a payload written by either producer renders.
|
|
47
|
+
*/
|
|
48
|
+
export declare function readCopilotPlanPayload(payload: Record<string, unknown> | null | undefined): {
|
|
49
|
+
summary: string;
|
|
50
|
+
steps: CopilotPlanRawStep[];
|
|
51
|
+
} | null;
|
|
52
|
+
/** Where a step's ETA came from. Rendered beside the number, never hidden. */
|
|
53
|
+
export type CopilotPlanEtaBasis =
|
|
54
|
+
/** A model estimate scaled by how wrong its estimates have been so far. */
|
|
55
|
+
"calibrated"
|
|
56
|
+
/** No usable estimate; the mean of this turn's own completed steps. */
|
|
57
|
+
| "measured"
|
|
58
|
+
/** The model's estimate, with nothing completed yet to calibrate it. */
|
|
59
|
+
| "model"
|
|
60
|
+
/** Nothing to go on. The surface must say so rather than invent a number. */
|
|
61
|
+
| "none";
|
|
62
|
+
export type CopilotPlanSubstep = {
|
|
63
|
+
id: string;
|
|
64
|
+
title: string;
|
|
65
|
+
status: CopilotPlanStepStatus;
|
|
66
|
+
/** `model` when the model declared it, `tool` when it is an observed call. */
|
|
67
|
+
source: "model" | "tool";
|
|
68
|
+
durationMs: number | null;
|
|
69
|
+
startedAt: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Set only for a `subagent_run`. A sub-agent's own capability calls never reach
|
|
72
|
+
* the ledger (it is handed capability tools directly, bypassing the runtime tool
|
|
73
|
+
* path), so the rollup counts are all there is -- and the rail must not imply a
|
|
74
|
+
* deeper trace exists.
|
|
75
|
+
*/
|
|
76
|
+
subagent?: {
|
|
77
|
+
name: string;
|
|
78
|
+
inferences: number;
|
|
79
|
+
capabilityCalls: number;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
export type CopilotPlanStep = {
|
|
83
|
+
id: string;
|
|
84
|
+
title: string;
|
|
85
|
+
status: CopilotPlanStepStatus;
|
|
86
|
+
estimateSeconds: number | null;
|
|
87
|
+
startedAt: string | null;
|
|
88
|
+
endedAt: string | null;
|
|
89
|
+
/** Wall time the step has taken, or took. Null until it starts. */
|
|
90
|
+
elapsedMs: number | null;
|
|
91
|
+
/** Seconds still expected. Null for a done step, or when nothing can be said. */
|
|
92
|
+
etaSeconds: number | null;
|
|
93
|
+
etaBasis: CopilotPlanEtaBasis;
|
|
94
|
+
substeps: CopilotPlanSubstep[];
|
|
95
|
+
};
|
|
96
|
+
export type CopilotPlanProjection = {
|
|
97
|
+
summary: string | null;
|
|
98
|
+
steps: CopilotPlanStep[];
|
|
99
|
+
progress: {
|
|
100
|
+
done: number;
|
|
101
|
+
total: number;
|
|
102
|
+
ratio: number;
|
|
103
|
+
};
|
|
104
|
+
eta: {
|
|
105
|
+
remainingSeconds: number | null;
|
|
106
|
+
basis: CopilotPlanEtaBasis;
|
|
107
|
+
/** How many completed, timed steps the measurement rests on. */
|
|
108
|
+
measuredSteps: number;
|
|
109
|
+
/** True when the turn's wall-clock deadline, not the plan, is the binding bound. */
|
|
110
|
+
deadlineBound: boolean;
|
|
111
|
+
};
|
|
112
|
+
/** False when no turn is executing: the plan is history, not work in flight. */
|
|
113
|
+
turnActive: boolean;
|
|
114
|
+
/** Ledger position of the newest plan_updated, so a caller can tell staleness. */
|
|
115
|
+
updatedAtSeq: number;
|
|
116
|
+
updatedAt: string;
|
|
117
|
+
};
|
|
118
|
+
export type CopilotPlanSourceEvent = {
|
|
119
|
+
seq: number;
|
|
120
|
+
kind: string;
|
|
121
|
+
payload: Record<string, unknown> | null;
|
|
122
|
+
created_at: string | Date;
|
|
123
|
+
};
|
|
124
|
+
export type ProjectCopilotPlanInput = {
|
|
125
|
+
/** Ordered by seq. Only plan_updated / tool_call_* / context_assembled are read. */
|
|
126
|
+
events: CopilotPlanSourceEvent[];
|
|
127
|
+
/** The active turn's wall-clock deadline, used only to clamp the total. */
|
|
128
|
+
turnDeadlineAt?: string | Date | null;
|
|
129
|
+
/**
|
|
130
|
+
* Whether a turn is executing right now. Defaults to true.
|
|
131
|
+
*
|
|
132
|
+
* A plan OUTLIVES the turn that wrote it. The model routinely stops without
|
|
133
|
+
* marking its last step done, so the session rests in the ledger forever with a
|
|
134
|
+
* step still `in_progress`. Read against a running clock that step accrues
|
|
135
|
+
* elapsed time indefinitely -- production session fd259942 reached 37 DAYS --
|
|
136
|
+
* and the surface goes on offering "time remaining" for work that stopped weeks
|
|
137
|
+
* ago. Both are the same error: treating a historical record as work in flight.
|
|
138
|
+
*
|
|
139
|
+
* So the clock stops with the turn, and a plan nobody is working reports no
|
|
140
|
+
* estimate at all rather than a false one.
|
|
141
|
+
*/
|
|
142
|
+
turnActive?: boolean;
|
|
143
|
+
now?: Date;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Rebuild the plan, with timings, from the session's event ledger.
|
|
147
|
+
*
|
|
148
|
+
* Returns null when the session has never published a plan -- which is the correct
|
|
149
|
+
* answer for a trivial turn, since the prompt tells the model to skip `plan_update`
|
|
150
|
+
* entirely and call `finish`. A rail that renders an empty shell for every greeting
|
|
151
|
+
* would be worse than one that stays away.
|
|
152
|
+
*/
|
|
153
|
+
export declare function projectCopilotPlan(input: ProjectCopilotPlanInput): CopilotPlanProjection | null;
|
|
154
|
+
/**
|
|
155
|
+
* How long, in words. One owner, because there were three and all three were wrong.
|
|
156
|
+
*
|
|
157
|
+
* The rail, `oxygen copilot plan` and the MCP widget each carried their own copy
|
|
158
|
+
* of this, and every copy did `Math.floor(s / 60)` minutes with `Math.round(s % 60)`
|
|
159
|
+
* seconds -- which rounds the remainder INDEPENDENTLY of the minutes it was taken
|
|
160
|
+
* from. A 179.6s sub-agent therefore rendered as "2m 60s" on all three surfaces
|
|
161
|
+
* (observed live in session 34826105), and 59.6s rendered as "60s" rather than
|
|
162
|
+
* "1m". Rounding the total FIRST and splitting afterwards cannot produce either.
|
|
163
|
+
*
|
|
164
|
+
* The projection promised one implementation behind three surfaces; the numbers
|
|
165
|
+
* were shared and the words describing them were not, so this is where they meet.
|
|
166
|
+
*/
|
|
167
|
+
export declare function formatCopilotPlanSeconds(seconds: number): string;
|
|
168
|
+
/** A duration in ms, or null when there is nothing worth showing. */
|
|
169
|
+
export declare function formatCopilotPlanDuration(ms: number | null | undefined): string | null;
|