@executablemd/test-agent 0.0.0-bootstrap.0 → 0.5.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/LICENSE +21 -0
- package/esm/mod.js +21 -0
- package/esm/package.json +3 -0
- package/esm/src/controller.js +295 -0
- package/esm/src/net.js +140 -0
- package/esm/src/protocol.js +144 -0
- package/esm/src/state.js +57 -0
- package/esm/src/template.js +138 -0
- package/esm/src/vocabulary.js +287 -0
- package/esm/src/worker/acp-server.js +138 -0
- package/esm/src/worker/bridge.js +84 -0
- package/esm/src/worker/profile.js +86 -0
- package/esm/src/worker/run.js +291 -0
- package/esm/src/worker/when-prompt.js +147 -0
- package/package.json +35 -8
- package/types/mod.d.ts +28 -0
- package/types/src/controller.d.ts +50 -0
- package/types/src/net.d.ts +51 -0
- package/types/src/protocol.d.ts +89 -0
- package/types/src/state.d.ts +27 -0
- package/types/src/template.d.ts +41 -0
- package/types/src/vocabulary.d.ts +29 -0
- package/types/src/worker/acp-server.d.ts +27 -0
- package/types/src/worker/bridge.d.ts +42 -0
- package/types/src/worker/profile.d.ts +20 -0
- package/types/src/worker/run.d.ts +20 -0
- package/types/src/worker/when-prompt.d.ts +11 -0
- package/README.md +0 -5
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node↔Effection boundary for line-framed TCP (specs/test-agent-spec.md
|
|
3
|
+
* §Controller and worker). A server that owns its listener and dispatches
|
|
4
|
+
* connections, a socket whose inbound lines are an Effection stream, and a
|
|
5
|
+
* client that shares the same socket adapter — so callers work with
|
|
6
|
+
* operations and streams, never raw `.on`/`.once`/Promise plumbing.
|
|
7
|
+
*/
|
|
8
|
+
import type { Operation, Stream } from "effection";
|
|
9
|
+
import type { Socket } from "node:net";
|
|
10
|
+
export interface LineSocket {
|
|
11
|
+
/** Inbound newline-framed lines; closes when the socket ends. */
|
|
12
|
+
lines: Stream<string, unknown>;
|
|
13
|
+
/** Write one already-framed line. */
|
|
14
|
+
send(line: string): void;
|
|
15
|
+
/** Graceful half-close: flush pending writes, then send FIN. */
|
|
16
|
+
end(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Resolves when the socket closes — EOF, error, or teardown. Consumers
|
|
19
|
+
* race their line loop against this: `fromReadable` closes the line
|
|
20
|
+
* stream on `end` but not on an abrupt reset, so without it an
|
|
21
|
+
* `each(lines)` loop would block forever on a peer that vanishes.
|
|
22
|
+
*/
|
|
23
|
+
closed: Operation<void>;
|
|
24
|
+
}
|
|
25
|
+
/** Adapt a connected socket: inbound bytes become a line stream; the socket
|
|
26
|
+
* is destroyed on teardown. */
|
|
27
|
+
export declare function useLineSocket(socket: Socket): Operation<LineSocket>;
|
|
28
|
+
export interface LineServer {
|
|
29
|
+
port: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A localhost line-protocol server as a resource. It listens, then accepts
|
|
33
|
+
* each connection, adapts it with {@link useLineSocket}, and runs
|
|
34
|
+
* `onConnection` for it in its own task — so a caller never spawns or
|
|
35
|
+
* touches a raw socket. The per-connection task is what lets simultaneous
|
|
36
|
+
* connections run concurrently (`each` itself is sequential).
|
|
37
|
+
*/
|
|
38
|
+
export declare function useLineServer(host: string, onConnection: (connection: LineSocket) => Operation<void>): Operation<LineServer>;
|
|
39
|
+
export interface LineClient<T> {
|
|
40
|
+
send(line: string): void;
|
|
41
|
+
/** The next parsed inbound message; throws once the connection closes. */
|
|
42
|
+
next(): Operation<T>;
|
|
43
|
+
/** Resolves when the connection closes. */
|
|
44
|
+
closed: Operation<void>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Connect to a line-protocol server. Shares {@link useLineSocket}, adds the
|
|
48
|
+
* connect handshake and a parsed inbound queue behind `next()` — all as
|
|
49
|
+
* operations. `parse` returning `undefined` drops an unparseable line.
|
|
50
|
+
*/
|
|
51
|
+
export declare function useLineClient<T>(host: string, port: number, parse: (line: string) => T | undefined): Operation<LineClient<T>>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controller ↔ worker wire protocol (specs/test-agent-spec.md
|
|
3
|
+
* §Controller and worker): newline-delimited JSON over localhost TCP.
|
|
4
|
+
* Every inbound line is parsed into a validated type — malformed,
|
|
5
|
+
* unknown, and directionally invalid messages are rejected, never cast.
|
|
6
|
+
*/
|
|
7
|
+
import type { DurableEvent } from "@executablemd/durable-streams";
|
|
8
|
+
/** The validated journal-event wire shape; structurally a DurableEvent. */
|
|
9
|
+
export type WireDurableEvent = DurableEvent;
|
|
10
|
+
export type WorkerMessage = {
|
|
11
|
+
t: "attach";
|
|
12
|
+
token: string;
|
|
13
|
+
instance: string;
|
|
14
|
+
} | {
|
|
15
|
+
t: "journal";
|
|
16
|
+
seq: number;
|
|
17
|
+
event: WireDurableEvent;
|
|
18
|
+
} | {
|
|
19
|
+
t: "read";
|
|
20
|
+
path: string;
|
|
21
|
+
} | {
|
|
22
|
+
t: "stat";
|
|
23
|
+
path: string;
|
|
24
|
+
} | {
|
|
25
|
+
t: "turn-failure";
|
|
26
|
+
kind: "mismatch" | "exhausted" | "config";
|
|
27
|
+
expected?: string;
|
|
28
|
+
actual: string;
|
|
29
|
+
} | {
|
|
30
|
+
t: "fatal";
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
export type ControllerMessage = {
|
|
34
|
+
t: "config";
|
|
35
|
+
mode: "probe";
|
|
36
|
+
} | {
|
|
37
|
+
t: "config";
|
|
38
|
+
mode: "scenario";
|
|
39
|
+
doc: {
|
|
40
|
+
path: string;
|
|
41
|
+
source: string;
|
|
42
|
+
};
|
|
43
|
+
journal: WireDurableEvent[];
|
|
44
|
+
} | {
|
|
45
|
+
t: "ack";
|
|
46
|
+
seq: number;
|
|
47
|
+
} | {
|
|
48
|
+
t: "recorded";
|
|
49
|
+
} | {
|
|
50
|
+
t: "read";
|
|
51
|
+
path: string;
|
|
52
|
+
source?: string;
|
|
53
|
+
missing: boolean;
|
|
54
|
+
} | {
|
|
55
|
+
t: "stat";
|
|
56
|
+
path: string;
|
|
57
|
+
exists: boolean;
|
|
58
|
+
isFile: boolean;
|
|
59
|
+
} | {
|
|
60
|
+
t: "error";
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
export declare function encodeMessage(message: WorkerMessage | ControllerMessage): string;
|
|
64
|
+
export type ParseResult<T> = {
|
|
65
|
+
ok: true;
|
|
66
|
+
message: T;
|
|
67
|
+
} | {
|
|
68
|
+
ok: false;
|
|
69
|
+
error: string;
|
|
70
|
+
};
|
|
71
|
+
export declare function parseWorkerMessage(line: string): ParseResult<WorkerMessage>;
|
|
72
|
+
export declare function parseControllerMessage(line: string): ParseResult<ControllerMessage>;
|
|
73
|
+
/** Incremental newline splitter: feed chunks, receive complete lines. */
|
|
74
|
+
export declare function createLineSplitter(): {
|
|
75
|
+
feed(chunk: string): string[];
|
|
76
|
+
};
|
|
77
|
+
export interface ParsedRoute {
|
|
78
|
+
host: string;
|
|
79
|
+
port: number;
|
|
80
|
+
token: string;
|
|
81
|
+
instance: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Routes are opaque to ACPX and workers alike; this is the one place
|
|
85
|
+
* that knows the encoding: host:port/token/instance.
|
|
86
|
+
*/
|
|
87
|
+
export declare function formatRoute(route: ParsedRoute): string;
|
|
88
|
+
export declare function parseRoute(value: string): ParseResult<ParsedRoute>;
|
|
89
|
+
export declare const PROBE_INSTANCE = "probe";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-boundary ACPX state for `<TestAgent>` (specs/test-agent-spec.md
|
|
3
|
+
* §Scenario instances): the production provider state composed with an
|
|
4
|
+
* in-memory session store and a dynamic registry whose resolve() embeds
|
|
5
|
+
* the pending instance route into the worker command. Routing flows
|
|
6
|
+
* through the provider's `sessionRouting` seam: the global route slot is
|
|
7
|
+
* held only across the provider's registry-dependent work (preparation
|
|
8
|
+
* and ensure/session validation + turn start), released while the
|
|
9
|
+
* provider waits on the per-session queue and during turn consumption.
|
|
10
|
+
*/
|
|
11
|
+
import type { Operation } from "effection";
|
|
12
|
+
import type { AcpxProviderSeams, AcpxProviderState, SessionRoutingContext } from "@executablemd/acp";
|
|
13
|
+
import type { AcpSessionStore } from "acpx/runtime";
|
|
14
|
+
export interface TestAgentAcpx {
|
|
15
|
+
state: AcpxProviderState;
|
|
16
|
+
}
|
|
17
|
+
export declare function createMemorySessionStore(): AcpSessionStore;
|
|
18
|
+
export interface TestAgentAcpxOptions {
|
|
19
|
+
defaultAgent: string;
|
|
20
|
+
agents: string[];
|
|
21
|
+
workerCommand: string[];
|
|
22
|
+
probeRoute: string;
|
|
23
|
+
/** Map a routing context to the instance route pinned for its work. */
|
|
24
|
+
routeFor(context: SessionRoutingContext): string;
|
|
25
|
+
seams?: AcpxProviderSeams;
|
|
26
|
+
}
|
|
27
|
+
export declare function useTestAgentAcpx(options: TestAgentAcpxOptions): Operation<TestAgentAcpx>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhenPrompt template matching (specs/test-agent-spec.md §WhenPrompt
|
|
3
|
+
* templates). Pure functions — no Effection — so every rule is unit
|
|
4
|
+
* testable: whole-prompt anchoring, `{?name}` captures, `{binding}`
|
|
5
|
+
* constraints, repeated-capture agreement, and adjacent-capture
|
|
6
|
+
* ambiguity.
|
|
7
|
+
*/
|
|
8
|
+
export type TemplateToken = {
|
|
9
|
+
kind: "literal";
|
|
10
|
+
text: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: "capture";
|
|
13
|
+
name: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "binding";
|
|
16
|
+
path: string;
|
|
17
|
+
};
|
|
18
|
+
export interface ParsedTemplate {
|
|
19
|
+
source: string;
|
|
20
|
+
tokens: TemplateToken[];
|
|
21
|
+
captureNames: string[];
|
|
22
|
+
}
|
|
23
|
+
export type TemplateParseResult = {
|
|
24
|
+
ok: true;
|
|
25
|
+
template: ParsedTemplate;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
error: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function parseTemplate(source: string): TemplateParseResult;
|
|
31
|
+
export type TemplateMatchResult = {
|
|
32
|
+
ok: true;
|
|
33
|
+
captures: Record<string, string>;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
kind: "mismatch" | "config";
|
|
37
|
+
expected: string;
|
|
38
|
+
actual: string;
|
|
39
|
+
message: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function matchPrompt(template: ParsedTemplate, prompt: string, env: Record<string, unknown>): TemplateMatchResult;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<TestAgent>` vocabulary (specs/test-agent-spec.md §TestAgent).
|
|
3
|
+
*
|
|
4
|
+
* `installTestAgentVocabulary` must be installed BEFORE
|
|
5
|
+
* `installAgentVocabulary` in the same scope: in-scope middleware runs
|
|
6
|
+
* in install order, so the global `<Prompt>` interceptor here sees the
|
|
7
|
+
* invocation first, forces `throwOnError` only when both `<TestAgent>`
|
|
8
|
+
* and `<Test>` are active, and otherwise delegates unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Each `<Test>` receives fresh ACPX state keyed by its lease EvalScope;
|
|
11
|
+
* the state is provisioned by a suspended task spawned into that scope,
|
|
12
|
+
* so halting the lease tears the provider down (canceling turns and
|
|
13
|
+
* closing workers) and removes the map entry on normal and failure
|
|
14
|
+
* paths alike. Outside a `<Test>`, the `<TestAgent>` scope itself is
|
|
15
|
+
* the isolation boundary.
|
|
16
|
+
*
|
|
17
|
+
* Scenario instances are resources held by suspended tasks spawned into
|
|
18
|
+
* their boundary scope, so a boundary halt releases them. Because
|
|
19
|
+
* acquiring one is an operation but the provider's route resolver is a
|
|
20
|
+
* synchronous callback, every instance is provisioned by the `session`
|
|
21
|
+
* and `prompt` seams before the provider routes, and `routeFor` is a
|
|
22
|
+
* pure lookup.
|
|
23
|
+
*/
|
|
24
|
+
import type { Operation } from "effection";
|
|
25
|
+
export interface TestAgentVocabularyOptions {
|
|
26
|
+
/** Command segments that relaunch this xmd as `test-agent`. */
|
|
27
|
+
workerCommand: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function installTestAgentVocabulary(options: TestAgentVocabularyOptions): Operation<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP-on-stdio serving for `xmd test-agent` (specs/test-agent-spec.md
|
|
3
|
+
* §Controller and worker). stdout carries only JSON-RPC lines; all
|
|
4
|
+
* logging goes to stderr. The worker is stateless but advertises and
|
|
5
|
+
* serves session/load: all state loads from the controller, so a load
|
|
6
|
+
* simply reuses the prior session id over the rehydrated document.
|
|
7
|
+
*/
|
|
8
|
+
import type { Operation } from "effection";
|
|
9
|
+
export type TurnResult = {
|
|
10
|
+
cancelled: true;
|
|
11
|
+
} | {
|
|
12
|
+
cancelled: false;
|
|
13
|
+
text: string;
|
|
14
|
+
};
|
|
15
|
+
export interface WorkerAgent {
|
|
16
|
+
/** Resolves once the behavior document reached its first matcher. */
|
|
17
|
+
ready(): Operation<void>;
|
|
18
|
+
runTurn(text: string): Operation<TurnResult>;
|
|
19
|
+
cancel(): void;
|
|
20
|
+
}
|
|
21
|
+
/** The byte transport the ACP connection is served over. */
|
|
22
|
+
export interface AcpByteStreams {
|
|
23
|
+
input: ReadableStream<Uint8Array>;
|
|
24
|
+
output: WritableStream<Uint8Array>;
|
|
25
|
+
}
|
|
26
|
+
export declare function serveAcp(worker: WorkerAgent, streams: AcpByteStreams): Operation<void>;
|
|
27
|
+
export declare function useProcessStdio(): Operation<AcpByteStreams>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker bridge (specs/test-agent-spec.md §Behavior documents): one
|
|
3
|
+
* ordered channel carries output chunks, matcher suspensions, EOF, and
|
|
4
|
+
* document failure, so the final output chunk of a turn always precedes
|
|
5
|
+
* the suspension/EOF signal that ends it. Prompt offers flow the other
|
|
6
|
+
* way, one at a time, to whichever matcher is suspended.
|
|
7
|
+
*/
|
|
8
|
+
import type { Channel, Operation, Subscription } from "effection";
|
|
9
|
+
import type { TemplateMatchResult } from "../template.js";
|
|
10
|
+
export type BridgeEvent = {
|
|
11
|
+
kind: "output";
|
|
12
|
+
text: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "suspended";
|
|
15
|
+
stage: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "eof";
|
|
18
|
+
} | {
|
|
19
|
+
kind: "failed";
|
|
20
|
+
error: string;
|
|
21
|
+
};
|
|
22
|
+
export interface PromptOffer {
|
|
23
|
+
text: string;
|
|
24
|
+
respond(outcome: TemplateMatchResult): void;
|
|
25
|
+
}
|
|
26
|
+
export interface TurnBridge {
|
|
27
|
+
events: Channel<BridgeEvent, never>;
|
|
28
|
+
offer(text: string): Operation<TemplateMatchResult>;
|
|
29
|
+
nextOffer(): Operation<PromptOffer>;
|
|
30
|
+
}
|
|
31
|
+
export declare function createTurnBridge(): TurnBridge;
|
|
32
|
+
/**
|
|
33
|
+
* Read bridge events until the current turn ends, concatenating output.
|
|
34
|
+
* Returns the terminal signal so callers distinguish the next matcher,
|
|
35
|
+
* EOF, and document failure.
|
|
36
|
+
*/
|
|
37
|
+
export declare function collectTurn(subscription: Subscription<BridgeEvent, never>): Operation<{
|
|
38
|
+
text: string;
|
|
39
|
+
end: "suspended" | "eof" | "failed";
|
|
40
|
+
stage?: string;
|
|
41
|
+
error?: string;
|
|
42
|
+
}>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic worker profile (specs/test-agent-spec.md §Deterministic
|
|
3
|
+
* runtime): a capability policy, not a security sandbox. Process and
|
|
4
|
+
* Fetch are denied, env() is undefined, cwd() is the virtual scenario
|
|
5
|
+
* root, and the filesystem is limited to controller-backed reads and
|
|
6
|
+
* stats. Reading a .ts component candidate raises the explicit
|
|
7
|
+
* unsupported-TypeScript error before it could ever be materialized or
|
|
8
|
+
* imported; stats stay honest so an earlier Name.md wins and a missing
|
|
9
|
+
* Name.ts falls through to Name/index.md. Eval blocks are inline-only:
|
|
10
|
+
* core strips static imports into options.imports before compiling, so
|
|
11
|
+
* a non-empty list is rejected, and the transformed source is parsed
|
|
12
|
+
* with acorn to reject dynamic import expressions before compilation.
|
|
13
|
+
*/
|
|
14
|
+
import type { Operation } from "effection";
|
|
15
|
+
import type { StatResult } from "@executablemd/runtime";
|
|
16
|
+
export interface WorkerFilesystem {
|
|
17
|
+
read(path: string): Operation<string | undefined>;
|
|
18
|
+
stat(path: string): Operation<StatResult>;
|
|
19
|
+
}
|
|
20
|
+
export declare function installWorkerProfile(filesystem: WorkerFilesystem): Operation<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xmd test-agent` worker runtime (specs/test-agent-spec.md §Controller
|
|
3
|
+
* and worker). The worker attaches to its controller, rehydrates the
|
|
4
|
+
* behavior document and journal, and serves ACP on stdio.
|
|
5
|
+
*
|
|
6
|
+
* A stage's buffered durable-event suffix — everything through the next
|
|
7
|
+
* suspension point or the final root Close — is forwarded and
|
|
8
|
+
* acknowledged before the ACP result is returned; a crash inside that
|
|
9
|
+
* final delivery window is outside the supported recovery guarantee.
|
|
10
|
+
*
|
|
11
|
+
* Cancellation is transactional from the controller journal's
|
|
12
|
+
* perspective: a cancelled turn commits nothing — the scenario runtime
|
|
13
|
+
* is halted and rebuilt from the last acknowledged journal before
|
|
14
|
+
* another turn is accepted, so the next prompt re-enters the same
|
|
15
|
+
* stage deterministically.
|
|
16
|
+
*/
|
|
17
|
+
import type { Operation } from "effection";
|
|
18
|
+
export declare function runTestAgentWorker(options: {
|
|
19
|
+
connect: string;
|
|
20
|
+
}): Operation<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<WhenPrompt>` vocabulary (specs/test-agent-spec.md §Behavior
|
|
3
|
+
* documents). Each matcher signals the bridge that the previous stage's
|
|
4
|
+
* rendering is complete, then suspends until an offered prompt matches.
|
|
5
|
+
* A match is one durable `when_prompt` operation whose record restores
|
|
6
|
+
* the matched prompt and captures on replay, so a rehydrated worker
|
|
7
|
+
* advances to the active matcher without re-matching.
|
|
8
|
+
*/
|
|
9
|
+
import type { Operation } from "effection";
|
|
10
|
+
import type { TurnBridge } from "./bridge.js";
|
|
11
|
+
export declare function installWhenPromptVocabulary(bridge: TurnBridge): Operation<void>;
|