@coreplane/switchboard 1.213.0 → 1.214.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.
@@ -145,8 +145,18 @@ export interface RunRecord {
145
145
  * item 48), stored at the claim so a retried spawn finds its run. Present
146
146
  * exactly when `parentInstanceId` is — both or neither, never one alone. */
147
147
  idempotencyKey?: string;
148
+ /** Where the run's conversation started (item 52): `channel` — its own
149
+ * thread's history, as for every run a person, a schedule or a coordinator
150
+ * started — or `parent` — a spawned child seeded from its parent's text
151
+ * turns at the spawn (`DispatchOptions.seed`). Absent on records written
152
+ * before it existed. */
153
+ seed?: RunSeed;
148
154
  }
149
155
 
156
+ /** The two places a run's conversation can start (item 52). */
157
+ export const RUN_SEEDS = ["channel", "parent"] as const;
158
+ export type RunSeed = (typeof RUN_SEEDS)[number];
159
+
150
160
  /** The profile as the record stores it: the run's effective profile plus the preset it came from. */
151
161
  export type RunProfileRecord = RunProfile & { preset: string };
152
162
 
@@ -515,6 +525,8 @@ export function isRunRecord(v: unknown): v is RunRecord {
515
525
  // A parent is named by a run id (item 46): the same shape as the record's own.
516
526
  if (r.parentRunId !== undefined && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
517
527
  return false;
528
+ // Where the conversation started (item 52): one of the two words, or absent.
529
+ if (r.seed !== undefined && !RUN_SEEDS.includes(r.seed as RunSeed)) return false;
518
530
  // A coordinator's child (item 48): the instance id in the platform's alphabet
519
531
  // and the key `<instance>:<step>` — both or neither; one alone is no tag.
520
532
  if ((r.parentInstanceId === undefined) !== (r.idempotencyKey === undefined)) return false;
@@ -52,6 +52,8 @@ export function shimRoute(pathname: string): string | undefined {
52
52
  if (pathname === "/costs" || pathname.startsWith("/costs/")) return "costs";
53
53
  if (pathname.startsWith("/api/")) return "api";
54
54
  if (pathname.startsWith("/admin/")) return "admin";
55
+ // The artifact copy (deploy/cloudflare/artifactsCopy.ts): the shim's own route, a 1 GB stream.
56
+ if (pathname === "/artifacts/copy") return "artifacts";
55
57
  // The model proxy's two routes (docs/reference/specs/model-proxy.md): a bounded
56
58
  // request per model call, forwarded to the container like everything else.
57
59
  if (pathname === "/v1/messages" || pathname === "/v1/chat/completions") return "model-proxy";
@@ -89,6 +89,17 @@ export const profileSchema = z.object({
89
89
  /** The Cloudflare Access application in front of the bot's dashboards, when
90
90
  * there is one: the team domain the JWT is issued by and the app's AUD. */
91
91
  access: z.object({ teamDomain: hostname, aud: z.string().regex(/^[0-9a-f]{64}$/) }).optional(),
92
+ /** The artifact store's bucket (docs/reference/specs/execution.md item 20), when the installation
93
+ * has one: the bot Worker's template binds it as `ARTIFACTS` and `deploy` creates it before the
94
+ * upload. The bot's runtime config (`artifacts.r2.bucket`) must name the same bucket — the two
95
+ * are held equal by `artifacts check`, not by the profile. R2's own bucket-name rules. */
96
+ artifacts: z
97
+ .object({
98
+ bucket: z
99
+ .string()
100
+ .regex(/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/, "an R2 bucket name: 3–63 lowercase letters, digits and hyphens"),
101
+ })
102
+ .optional(),
92
103
  });
93
104
 
94
105
  export type DeploymentProfile = z.infer<typeof profileSchema>;
@@ -19,6 +19,13 @@ import type { ResidentLifecycleState } from "./residentState.js";
19
19
  * for the runtime-replaced guidance (~120); short enough for a card note. */
20
20
  export const RESIDENT_TEXT_CAP = 300;
21
21
 
22
+ /** Cap for a resident `error`. A refusal is a whole reply, not a card note: the
23
+ * onboard not-in-installation 403 (item 35) carries two causes, the admin's
24
+ * path to the fix and GitHub's own 422 body — ~600 chars, and every word past
25
+ * the first 300 is the one the admin acts on. Still a bound, so a hostile body
26
+ * cannot make a reply unbounded; Slack's 3000-char section stays far away. */
27
+ export const RESIDENT_ERROR_CAP = 1000;
28
+
22
29
  /** Strip terminal control sequences, redact credential shapes, cap. The identity
23
30
  * on the discriminator literals the bot compares (`runtime-replaced`,
24
31
  * `op-refused…`, `disk-pressure:`), pinned by test. */
@@ -26,6 +33,11 @@ export function residentText(text: string): string {
26
33
  return redactAndCap(stripAnsi(text), RESIDENT_TEXT_CAP);
27
34
  }
28
35
 
36
+ /** The same strip and redact, at the reply-sized cap — for `error` only. */
37
+ export function residentErrorText(text: string): string {
38
+ return redactAndCap(stripAnsi(text), RESIDENT_ERROR_CAP);
39
+ }
40
+
29
41
  const SANITIZED_FIELDS = ["error", "reason", "summary"] as const;
30
42
 
31
43
  /** How deep the sanitizer descends. The deepest resident shape today is
@@ -36,7 +48,8 @@ const SANITIZE_DEPTH = 4;
36
48
  /** A parsed resident body with its free-text fields made safe, at every level:
37
49
  * `/residents` nests each resident's `state`/`reason` (or an `error`) under
38
50
  * `residents[].live`, and `repo list` renders those. In each plain object only
39
- * `error`, `reason` and `summary` are touched (when strings); `stderr` is
51
+ * `error`, `reason` and `summary` are touched (when strings) — `error` at the
52
+ * reply-sized cap, the other two at the card-sized one; `stderr` is
40
53
  * rewritten only when it mirrors `error` (the thread routes' failure shape
41
54
  * copies the error into stderr). Every other field — `needs`, `state`,
42
55
  * `stdout`, `status`, bindings, numbers — passes through untouched. Arrays are
@@ -53,7 +66,9 @@ function walk(value: unknown, depth: number): unknown {
53
66
  for (const [key, v] of Object.entries(src)) {
54
67
  out[key] =
55
68
  (SANITIZED_FIELDS as readonly string[]).includes(key) && typeof v === "string"
56
- ? residentText(v)
69
+ ? key === "error"
70
+ ? residentErrorText(v)
71
+ : residentText(v)
57
72
  : walk(v, depth - 1);
58
73
  }
59
74
  if (typeof src.stderr === "string" && src.stderr === src.error && typeof out.error === "string") {
@@ -88,6 +88,12 @@ export interface CompletionRequest {
88
88
  system?: string;
89
89
  messages: ChatMessage[];
90
90
  tools?: ToolDef[];
91
+ /** Force one of `tools`: the model must answer by calling the named tool,
92
+ * so the call's input IS the answer and prose cannot occur (the request
93
+ * router's shape, routing-and-config item 21). Anthropic: `tool_choice:
94
+ * {type: "tool", name}`; Chat Completions: `tool_choice: {type: "function",
95
+ * function: {name}}`. Absent → the model chooses. */
96
+ toolChoice?: { type: "tool"; name: string };
91
97
  maxTokens: number;
92
98
  /** model effort hint; providers apply it only where the model supports it */
93
99
  effort?: Effort;