@cueloop/schema 0.1.0-alpha.97 → 0.1.0-alpha.98

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/schema",
3
- "version": "0.1.0-alpha.97",
3
+ "version": "0.1.0-alpha.98",
4
4
  "description": "The Thread primitive: types, markdown block model, quote-primary anchors, feedback serialization",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -0,0 +1,26 @@
1
+ import { expect, test } from "bun:test";
2
+ import { agentCommentRoot } from "./agent-comment";
3
+ import type { ThreadAgentState } from "./thread-agent";
4
+
5
+ const state: ThreadAgentState = {
6
+ threadId: "thread",
7
+ phase: { kind: "idle" },
8
+ messages: [],
9
+ tools: [],
10
+ comments: [],
11
+ submissions: [
12
+ { id: "prompt", commentId: "prompt", prompt: "Explain retries", status: "completed" },
13
+ ],
14
+ };
15
+
16
+ test("accepted bottom prompts have quote-primary read-only discussion roots", () => {
17
+ expect(agentCommentRoot(state, "prompt")).toMatchObject({
18
+ id: "prompt",
19
+ messageId: "prompt",
20
+ body: "Explain retries",
21
+ sent: true,
22
+ anchor: { quote: "Explain retries" },
23
+ });
24
+ expect(state.comments).toEqual([]);
25
+ expect(agentCommentRoot(state, "missing")).toBeUndefined();
26
+ });
@@ -0,0 +1,15 @@
1
+ import { makeAnchor } from "./anchor";
2
+ import { parseBlocks } from "./markdown";
3
+ import type { AgentComment, ThreadAgentState } from "./thread-agent";
4
+
5
+ /** Submitted bottom prompts are discussion roots without duplicating them in stored comments. */
6
+ export function agentCommentRoot(state: ThreadAgentState, id: string): AgentComment | undefined {
7
+ const comment = state.comments.find((entry) => entry.id === id);
8
+ if (comment) return comment;
9
+ const submission = state.submissions?.find((entry) => entry.id === id && entry.commentId === id);
10
+ if (!submission) return undefined;
11
+ const blocks = parseBlocks(submission.prompt);
12
+ const anchor = makeAnchor(blocks, 0, 0, Math.min(blocks[0]?.text.length ?? 0, 32768));
13
+
14
+ return { id, messageId: id, anchor, body: submission.prompt.slice(0, 32768), sent: true };
15
+ }
@@ -0,0 +1,71 @@
1
+ import type { AgentPermission, AgentTool, AgentConfigOption } from "./thread-agent";
2
+
3
+ /** Harness events carry transcript data without exposing a provider's wire protocol. */
4
+ export type AgentHarnessEvent =
5
+ | { kind: "message"; id?: string; text: string }
6
+ | AgentHarnessDiagnostic
7
+ | {
8
+ kind: "tool";
9
+ id: string;
10
+ title?: string;
11
+ toolKind?: string;
12
+ status?: AgentTool["status"];
13
+ output?: string;
14
+ locations?: AgentTool["locations"];
15
+ }
16
+ | { kind: "permission"; permission: AgentPermission }
17
+ | { kind: "config"; options: AgentConfigOption[] };
18
+
19
+ /** A completed turn delivers feedback; stopped or incomplete turns preserve it for retry. */
20
+ export interface AgentHarnessResult {
21
+ outcome: "completed" | "cancelled" | "incomplete";
22
+ }
23
+
24
+ /** One harness connection owns initialization, session restore, and its subprocess. */
25
+ export interface AgentHarnessConnection {
26
+ start(): Promise<string>;
27
+ prompt(text: string): Promise<AgentHarnessResult>;
28
+ cancel(): void;
29
+ permission(requestId: string, optionId?: string): void;
30
+ close(): void;
31
+ configure?(id: string, value: string): Promise<void>;
32
+ }
33
+
34
+ /** The daemon supplies callbacks and a workspace; executable configuration stays in its adapter. */
35
+ export interface AgentHarnessOptions {
36
+ cwd: string;
37
+ sessionId?: string;
38
+ tools?: AgentHarnessTools;
39
+ onEvent: (event: AgentHarnessEvent) => void;
40
+ onExit: (error: Error) => void;
41
+ }
42
+
43
+ /** A harness adapter can replace the agent runtime without changing Thread storage or rendering. */
44
+ export interface AgentHarnessAdapter {
45
+ id: string;
46
+ label: string;
47
+ connect(options: AgentHarnessOptions): AgentHarnessConnection;
48
+ }
49
+
50
+ /** Tools use the daemon API; call arguments and results are serialized JSON validated at the boundary. */
51
+ export interface AgentHarnessTools {
52
+ definitions: {
53
+ name: string;
54
+ description: string;
55
+ inputSchema: {
56
+ type: "object";
57
+ properties: Record<string, { type: string; enum?: string[] }>;
58
+ required: string[];
59
+ };
60
+ }[];
61
+ call(name: string, args: string): Promise<string>;
62
+ }
63
+
64
+ /** Harness diagnostics never enter conversation history; unknown severities retain their spelling. */
65
+ export interface AgentHarnessDiagnostic {
66
+ kind: "diagnostic";
67
+ severity: string;
68
+ title: string;
69
+ text: string;
70
+ source: "protocol" | "legacy-text";
71
+ }
@@ -0,0 +1,9 @@
1
+ import { expect, test } from "bun:test";
2
+ import { agentInputTarget } from "./agent-input";
3
+
4
+ test("typing targets a prompt, a marked comment, or neither", () => {
5
+ expect(agentInputTarget(true, false)).toBe("prompt");
6
+ expect(agentInputTarget(true, true)).toBe("prompt");
7
+ expect(agentInputTarget(false, true)).toBe("comment");
8
+ expect(agentInputTarget(false, false)).toBe("none");
9
+ });
@@ -0,0 +1,9 @@
1
+ /** A prompt line and a marked-text comment are mutually exclusive input targets. */
2
+ export type AgentInputTarget = "prompt" | "comment" | "none";
3
+
4
+ /** Unmarked artifact text cannot create a comment; the final prompt line never creates one. */
5
+ export function agentInputTarget(atPrompt: boolean, hasSelection: boolean): AgentInputTarget {
6
+ if (atPrompt) return "prompt";
7
+
8
+ return hasSelection ? "comment" : "none";
9
+ }
@@ -0,0 +1,15 @@
1
+ import { expect, test } from "bun:test";
2
+ import { stepAgentSubmission, type AgentSubmissionStatus } from "./agent-submission";
3
+
4
+ test("an accepted submission stays locked through failure and retry", () => {
5
+ expect(stepAgentSubmission("draft", "submit", false)).toBe("draft");
6
+ let state: AgentSubmissionStatus = stepAgentSubmission("draft", "submit", true);
7
+
8
+ for (const event of ["start", "fail", "retry", "start", "complete"] as const) {
9
+ state = stepAgentSubmission(state, event, true);
10
+ expect(state).not.toBe("draft");
11
+ expect(stepAgentSubmission(state, "edit", true)).toBe(state);
12
+ }
13
+ expect(state).toBe("completed");
14
+ expect(stepAgentSubmission(state, "retry", true)).toBe("completed");
15
+ });
@@ -0,0 +1,38 @@
1
+ /** Submitted comments stay read-only through queueing, completion, failure, and retry. */
2
+ export type AgentSubmissionStatus = "draft" | "queued" | "running" | "completed" | "failed";
3
+
4
+ /** Submission events never rewrite an accepted input. */
5
+ export type AgentSubmissionEvent = "submit" | "start" | "complete" | "fail" | "retry" | "edit";
6
+
7
+ /** Empty input cannot create a turn; retry is valid only for a failed submission. */
8
+ export function stepAgentSubmission(status: "draft", event: "submit", hasInput: true): "queued";
9
+ export function stepAgentSubmission(
10
+ status: Exclude<AgentSubmissionStatus, "draft">,
11
+ event: AgentSubmissionEvent,
12
+ hasInput: boolean,
13
+ ): Exclude<AgentSubmissionStatus, "draft">;
14
+ export function stepAgentSubmission(
15
+ status: AgentSubmissionStatus,
16
+ event: AgentSubmissionEvent,
17
+ hasInput: boolean,
18
+ ): AgentSubmissionStatus;
19
+ export function stepAgentSubmission(
20
+ status: AgentSubmissionStatus,
21
+ event: AgentSubmissionEvent,
22
+ hasInput: boolean,
23
+ ): AgentSubmissionStatus {
24
+ switch (event) {
25
+ case "submit":
26
+ return status === "draft" && hasInput ? "queued" : status;
27
+ case "start":
28
+ return status === "queued" ? "running" : status;
29
+ case "complete":
30
+ return status === "running" ? "completed" : status;
31
+ case "fail":
32
+ return status === "running" ? "failed" : status;
33
+ case "retry":
34
+ return status === "failed" ? "queued" : status;
35
+ case "edit":
36
+ return status;
37
+ }
38
+ }
@@ -0,0 +1,18 @@
1
+ import { expect, test } from "bun:test";
2
+ import { routeHarnessOutput } from "./harness-output-routing";
3
+
4
+ test("identical assistant and diagnostic wording takes different destinations", () => {
5
+ const text =
6
+ "[context] skill catalog shortened 112 descriptions: effective=20480 bytes source=compiled default\n";
7
+
8
+ expect(routeHarnessOutput({ kind: "message", text }).destination).toBe("thread");
9
+ expect(
10
+ routeHarnessOutput({
11
+ kind: "diagnostic",
12
+ text,
13
+ severity: "future-severity",
14
+ title: "Notice",
15
+ source: "protocol",
16
+ }).destination,
17
+ ).toBe("diagnostics");
18
+ });
@@ -0,0 +1,12 @@
1
+ import type { AgentHarnessDiagnostic, AgentHarnessEvent } from "./agent-harness";
2
+
3
+ type RoutedHarnessOutput =
4
+ | { destination: "diagnostics"; event: AgentHarnessDiagnostic }
5
+ | { destination: "thread"; event: Exclude<AgentHarnessEvent, AgentHarnessDiagnostic> };
6
+
7
+ /** Route normalized harness output by provenance; assistant wording never changes its destination. */
8
+ export function routeHarnessOutput(event: AgentHarnessEvent): RoutedHarnessOutput {
9
+ if (event.kind === "diagnostic") return { destination: "diagnostics", event };
10
+
11
+ return { destination: "thread", event };
12
+ }
package/src/index.ts CHANGED
@@ -13,3 +13,13 @@ export * from "./herdr";
13
13
  export * from "./thread-surface";
14
14
  export * from "./history";
15
15
  export * from "./path-view";
16
+ export * from "./thread-agent";
17
+ export * from "./agent-harness";
18
+
19
+ export * from "./agent-submission";
20
+
21
+ export { agentCommentRoot } from "./agent-comment";
22
+
23
+ export * from "./agent-input";
24
+
25
+ export * from "./harness-output-routing";
@@ -0,0 +1,80 @@
1
+ import type { Anchor } from "./types";
2
+
3
+ /** An agent message keeps its identity after streaming and across reconnects. */
4
+ export interface AgentMessage {
5
+ id: string;
6
+ role: "user" | "agent";
7
+ text: string;
8
+ complete: boolean;
9
+ revision: number;
10
+ submissionId?: string;
11
+ }
12
+
13
+ /** Tool activity belongs to a turn; its output is bounded by the daemon. */
14
+ export interface AgentTool {
15
+ id: string;
16
+ turnId: string;
17
+ title: string;
18
+ kind: string;
19
+ status: "pending" | "in_progress" | "completed" | "failed" | "cancelled";
20
+ output: string;
21
+ locations: { path: string; line?: number }[];
22
+ }
23
+
24
+ /** Agent feedback targets a completed message, never a changing stream. */
25
+ export interface AgentComment {
26
+ id: string;
27
+ messageId: string;
28
+ anchor: Anchor;
29
+ body: string;
30
+ sent: boolean;
31
+ replyTo?: string;
32
+ author?: string;
33
+ }
34
+
35
+ /** Permission options come from the active agent request, not client defaults. */
36
+ export interface AgentPermission {
37
+ id: string;
38
+ title: string;
39
+ options: { optionId: string; name: string; kind: string }[];
40
+ }
41
+
42
+ /** Agent lifecycle state distinguishes a waiting permission from an active turn. */
43
+ export type AgentPhase =
44
+ | { kind: "idle" }
45
+ | { kind: "running" }
46
+ | { kind: "permission"; permission: AgentPermission }
47
+ | { kind: "failed"; error: string };
48
+
49
+ /** A thread agent transcript is separate from the artifact and its revision history. */
50
+ export interface ThreadAgentState {
51
+ threadId: string;
52
+ /** Session identity is bound to its adapter; another harness cannot resume it. */
53
+ harness?: { id: string; label: string; sessionId?: string };
54
+ phase: AgentPhase;
55
+ messages: AgentMessage[];
56
+ tools: AgentTool[];
57
+ comments: AgentComment[];
58
+ submissions?: AgentSubmission[];
59
+ configOptions?: AgentConfigOption[];
60
+ }
61
+
62
+ /** A submission freezes its input; retry keeps the same mirror and discussion origin. */
63
+ export interface AgentSubmission {
64
+ id: string;
65
+ commentId?: string;
66
+ messageId?: string;
67
+ prompt: string;
68
+ quote?: string;
69
+ context?: string;
70
+ status: "queued" | "running" | "completed" | "failed";
71
+ }
72
+
73
+ /** Model and reasoning choices are advertised by the harness, never invented by the UI. */
74
+ export interface AgentConfigOption {
75
+ id: string;
76
+ name: string;
77
+ category?: string;
78
+ currentValue: string;
79
+ options: { value: string; name: string }[];
80
+ }