Skip to content

Commit 53cdd36

Browse files
authored
Merge pull request #989 from taras/agent/sidekick-acp-access-961
2 parents 71083a5 + e2e5742 commit 53cdd36

36 files changed

Lines changed: 4397 additions & 213 deletions

‎architecture.md‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6419,6 +6419,38 @@ projection detaches every value it retains and freezes it, so nothing below
64196419
holds a `DurableEvent` and nothing above can change one. Route resolution is
64206420
pure and answers with the exact objects that model holds.
64216421

6422+
### Local Sidekick attachments
6423+
6424+
The CLI-private `sidekick-acp` composition attaches observers and an ACP stdio
6425+
bridge to the live root's existing Sidekick dispatch. With `--sidekick-acp` and
6426+
no selected conversation, startup sends one real initialization message through
6427+
that dispatch and waits for joined completion before publishing the endpoint.
6428+
Existing selected history needs no initialization turn or provider setup; an
6429+
initialization failure is reported without publishing an endpoint. Ordinary
6430+
startup remains passive. Initialization retains captured provider configuration,
6431+
entry admission and pending/uncertain-effect rules. It introduces no evaluator,
6432+
journal writer or conversation controller. The root owns producers and issued
6433+
attachment mappings; each connection owns its bounded public observation and
6434+
request tasks. EOF releases only those observations. Explicit cancellation
6435+
captures an exact exchange before suspension and joins it; enclosing command
6436+
teardown joins both producer and transport resources.
6437+
6438+
A contextual transport is installed explicitly by each runtime entrypoint. Its
6439+
POSIX adapter owns asynchronous socket/file acquisition, exclusive complete
6440+
descriptor publication and identity-checked cleanup. Shared attachment modules
6441+
use that context and detached acknowledged reads. A descriptor binds its live
6442+
owner/execution/conversation/cwd, and every connection handshakes that exact
6443+
binding. Conversation selection changes invalidate it. Private protocol schemas
6444+
bound both framing and the read surface; an observer that exceeds its mailbox
6445+
cannot block the root producer.
6446+
6447+
The bridge distinguishes three facts: accepted saved message, joined exchange
6448+
completion and the product verdict inside evaluated Output. Provisional activity
6449+
is marked as such, and no native transcript crosses this boundary. Outer ACP
6450+
identity/materialization describes only the attachment and its message acceptance;
6451+
it does not assert acceptance by the underlying Sidekick provider. Unknown
6452+
feedback keeps its existing refusal and is never resent through attachment.
6453+
64226454
### Session workspace placement
64236455

64246456
A Sidekick response and an entry submitted through its controls start in the
@@ -8623,6 +8655,26 @@ every root job and subscription are children of that screen scope, so Exit,
86238655
Control-C, end of input and failure all unwind and join the Sidekick's work
86248656
before the command reports its outcome and the terminal is released.
86258657

8658+
**One root-owned dispatch spans the whole Sidekick exchange.** UI Send and
8659+
Retry use the same private service and admission policy as an attributed external
8660+
caller. Constructing an operation does nothing; executing it captures lexical
8661+
origin and conversation selection, then reserves before producer work. Message,
8662+
inspection, save and replacement share the existing policy: save and replacement
8663+
may accompany a message, while explicit configuration jobs exclude one another.
8664+
The producer belongs to the live command. Detaching an observer does not cancel
8665+
it or release its reservation. Exact-handle Stop joins producer cleanup before
8666+
release, and an old handle cannot stop a successor. Entry work remains owned by
8667+
ReplSession.
8668+
8669+
Every message record carries an opaque ID and a UI/attachment origin beside the
8670+
conversation-scoped numeric `turn`, and a record missing either is unreadable
8671+
rather than assumed. The reference is published only after the message append
8672+
is acknowledged.
8673+
ACP-origin acknowledgement clears neither composer nor draft takeover. The live
8674+
root indexes established IDs for passive observation; that index supplies no
8675+
restart, resend or provider-acceptance authority. An earlier answer followed by
8676+
a failed turn remains partial evidence in the failed exchange result.
8677+
86268678
**The owner reports its own changes to the loop, and that report carries no
86278679
authority.** A reply arriving, an automatic follow-up starting, a cycle failing
86288680
and a joined Stop are things no keystroke announces, so the owner owns a signal
@@ -8791,7 +8843,7 @@ and categorization rather than clarify them, so both stay exactly as written.
87918843
| background automation | work designed to proceed without ongoing user or agent attention, with its persistence and recovery guarantees supplied by the workflow host |
87928844
| REPL | the foreground interactive environment for experimenting with and testing components; replay restores recorded execution state, while the user or agent handles differences between that state and the live environment |
87938845
| REPL history | the immutable ordered execution records the REPL reads from the durable journal and projects into `ReplModel`; the History surface presents this read model and is not itself a journal [Reference](#the-private-repl-composition-kernel). |
8794-
| message ID | the number Sidekick retains to identify a saved person message within its conversation; fixed once saved, possibly noncontiguous, and independent of the turn numbers recorded by Core. The conversation and message ID together form its lookup key |
8846+
| message ID | the opaque identity Sidekick retains for a saved person message, written before its turn and never rewritten. The conversation and message ID together form its lookup key, and the conversation-scoped numeric `turn` beside it identifies no turn |
87958847
| turn | one request to an agent, together with the response or failure from that request |
87968848
| middleware | applied by the lexical structure, used by runtime execution [Reference](#two-layers). |
87978849
| workflow run | a workflow being carried out with its progress and outcome recorded durably; document executions perform its work, while ongoing effects remain scoped to the document execution in which they run [Reference](#workflow-runs). |

‎deno.lock‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/cli/deno.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@bomb.sh/tty": "npm:@bomb.sh/tty@0.9.0",
88
"@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.8.3",
99
"@standard-schema/spec": "npm:@standard-schema/spec@^1.0.0",
10-
"zod": "npm:zod@^4.3.6"
10+
"zod": "npm:zod@^4.3.6",
11+
"@agentclientprotocol/sdk": "npm:@agentclientprotocol/sdk@1.3.0"
1112
}
1213
}

‎packages/cli/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"default": "./src/node.ts"
1212
},
1313
"dependencies": {
14+
"@agentclientprotocol/sdk": "1.3.0",
1415
"@bomb.sh/tty": "0.9.0",
1516
"@effectionx/stream-helpers": "0.8.3",
1617
"@executablemd/acp": "workspace:*",

‎packages/cli/src/bun.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { usePosixSidekickTransport } from "./sidekick-acp/posix.ts";
12
/**
23
* Bun source entrypoint.
34
*
@@ -37,6 +38,7 @@ const UPGRADE: UpgradeAssembly = {
3738
};
3839

3940
await main(function* (args) {
41+
yield* usePosixSidekickTransport(process.platform);
4042
yield* useBunComponentModuleLoader();
4143
// The base providers for this host. `at: "min"` puts them beneath ordinary
4244
// middleware, so middleware installed later can wrap either one.

‎packages/cli/src/cli.ts‎

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { resolve } from "node:path";
1+
import { runSidekickCommand, SIDEKICK_HELP } from "./sidekick-acp/command.ts";
2+
import { SidekickTransport } from "./sidekick-acp/protocol.ts";
3+
import { isAbsolute, resolve } from "node:path";
24
import { parseWorkspaceMounts } from "./workspace-mounts.ts";
35
/**
46
* CLI — run an executable markdown document.
@@ -322,6 +324,10 @@ const REPL_DESCRIPTION = "Run and reconstruct one XMD entry in an interactive te
322324
* their entries is not the same choice as who their entries talk to.
323325
*/
324326
const replConfig = object({
327+
sidekickAcp: {
328+
description: "absolute private attachment file; initializes Sidekick when needed (POSIX only)",
329+
...field(z.string().optional()),
330+
},
325331
mount: {
326332
description: "external SOURCE:TARGET directory; repeat for each explicit TARGET",
327333
...field(z.array(z.string()), field.default([]), field.array()),
@@ -566,6 +572,7 @@ const xmd = program({
566572
agent: { ...agentConfig, description: AGENT_DESCRIPTION },
567573
upgrade: { ...upgradeConfig, description: UPGRADE_DESCRIPTION },
568574
repl: { ...replConfig, description: REPL_DESCRIPTION },
575+
sidekick: { description: "Attach to or passively read one live Sidekick.", ...object({}) },
569576
"test-agent": testAgentConfig,
570577
workflow: workflowConfig,
571578
},
@@ -665,7 +672,7 @@ export interface ReplHostAssembly {
665672
* silence would let them believe they got it.
666673
*/
667674
/**
668-
* The five REPL options that carry a value, by the spelling a caller writes.
675+
* The six REPL options that carry a value, by the spelling a caller writes.
669676
*
670677
* Exported because the documentation check reads this command's own examples,
671678
* and a token after one of these is that option's value rather than a location.
@@ -678,6 +685,7 @@ export const REPL_VALUES: readonly string[] = [
678685
"--agent-provider",
679686
"--default-agent",
680687
"--sidekick-agent",
688+
"--sidekick-acp",
681689
];
682690

683691
/** The three REPL switches, which carry none. */
@@ -691,6 +699,7 @@ export function replGrammarError(
691699
// reads its line directly.
692700
const rest = args.slice(1);
693701
const positional: string[] = [];
702+
let attachmentPath = false;
694703
for (let at = 0; at < rest.length; at += 1) {
695704
const token = rest[at] ?? "";
696705
if (!token.startsWith("-") || token === "-") {
@@ -708,6 +717,19 @@ export function replGrammarError(
708717
}
709718
continue;
710719
}
720+
if (name === "--sidekick-acp") {
721+
if (attachmentPath) {
722+
return "xmd repl: --sidekick-acp accepts exactly one path";
723+
}
724+
attachmentPath = true;
725+
const path = written ?? rest[at + 1];
726+
if (path === undefined || path.length === 0 || path.startsWith("-")) {
727+
return "xmd repl: --sidekick-acp requires a value";
728+
}
729+
if (!isAbsolute(path)) {
730+
return "xmd repl: --sidekick-acp requires an absolute path";
731+
}
732+
}
711733
if (REPL_VALUES.includes(name)) {
712734
if (written !== undefined) {
713735
if (written.length === 0) {
@@ -2516,6 +2538,7 @@ const COMMAND_NAMES = [
25162538
"upgrade",
25172539
"agent",
25182540
"repl",
2541+
"sidekick",
25192542
"test-agent",
25202543
"workflow",
25212544
];
@@ -2643,7 +2666,7 @@ function renderHelp(phase: PropsPhase): string {
26432666
: command === "agent"
26442667
? AGENT_OPTIONS_HELP
26452668
: command === "repl"
2646-
? REPL_HELP
2669+
? REPL_HELP + "\n\n" + SIDEKICK_HELP
26472670
: "";
26482671
const withSource = epilogue === "" ? base : `${base}\n\n${epilogue}`;
26492672

@@ -2952,6 +2975,14 @@ function* dispatch(
29522975
yield* exit(1);
29532976
break;
29542977
}
2978+
if (
2979+
command.config.sidekickAcp !== undefined &&
2980+
!(yield* SidekickTransport.get())?.supported()
2981+
) {
2982+
console.error("xmd repl: Sidekick ACP requires a same-user POSIX Unix socket.");
2983+
yield* exit(1);
2984+
break;
2985+
}
29552986
const mountCwd = yield* cwd();
29562987
const mounts = parseWorkspaceMounts(command.config.mount, mountCwd, { resolve });
29572988
if (!mounts.ok) {
@@ -3040,6 +3071,9 @@ function* dispatch(
30403071
yield* replHost.install();
30413072
const ran = yield* runReplProgram({
30423073
profile,
3074+
...(command.config.sidekickAcp === undefined
3075+
? {}
3076+
: { sidekickAcp: command.config.sidekickAcp }),
30433077
...(command.config.location === undefined ? {} : { location: command.config.location }),
30443078
});
30453079
if (!ran.ok) {
@@ -3433,6 +3467,17 @@ export function* runXmd(
34333467
// restore.
34343468
replHost?: ReplHostAssembly,
34353469
): Operation<void> {
3470+
if (args[0] === "sidekick") {
3471+
try {
3472+
yield* runSidekickCommand(args.slice(1));
3473+
} catch (error) {
3474+
console.error(
3475+
"xmd sidekick: " + (error instanceof Error ? error.message : "attachment refused"),
3476+
);
3477+
yield* exit(1);
3478+
}
3479+
return;
3480+
}
34363481
// Before every scanner and before anything reads a path: what a command line
34373482
// selects is read from the argv the caller wrote, and the tokens that
34383483
// selected it are removed from everything downstream.

‎packages/cli/src/compiled.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { usePosixSidekickTransport } from "./sidekick-acp/posix.ts";
12
/**
23
* Compiled-binary entrypoint — what `deno task build` compiles.
34
*
@@ -55,6 +56,7 @@ if (isCredentialHelperMode(process.argv.slice(2))) {
5556
await main(() => runCredentialHelper(process.argv.slice(2)));
5657
} else {
5758
await main(function* (args) {
59+
yield* usePosixSidekickTransport(process.platform);
5860
yield* useDenoComponentModuleLoader();
5961
// The base providers for this host. `at: "min"` puts them beneath ordinary
6062
// middleware, so middleware installed later can wrap either one.

‎packages/cli/src/deno.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { usePosixSidekickTransport } from "./sidekick-acp/posix.ts";
12
/**
23
* Deno source entrypoint.
34
*
@@ -71,6 +72,7 @@ if (isCredentialHelperMode(process.argv.slice(2))) {
7172
await main(() => runCredentialHelper(process.argv.slice(2)));
7273
} else {
7374
await main(function* (args) {
75+
yield* usePosixSidekickTransport(process.platform);
7476
yield* useDenoComponentModuleLoader();
7577
// The base providers for this host. `at: "min"` puts them beneath ordinary
7678
// middleware, so middleware installed later can wrap either one.

‎packages/cli/src/node.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { usePosixSidekickTransport } from "./sidekick-acp/posix.ts";
12
/**
23
* Node source and npm-package entrypoint.
34
*
@@ -44,6 +45,7 @@ const UPGRADE: UpgradeAssembly = {
4445
};
4546

4647
await main(function* (args) {
48+
yield* usePosixSidekickTransport(process.platform);
4749
yield* useNodeComponentModuleLoader();
4850
// The base providers for this host. `at: "min"` puts them beneath ordinary
4951
// middleware, so middleware installed later can wrap either one.

‎packages/cli/src/repl/application.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* adoption; a refusal preserves the standing route, selection and draft.
99
*/
1010

11+
import type { MessageOrigin } from "./sidekick/history.ts";
1112
import { Ok, type Result } from "effection";
1213
import type { Json } from "@executablemd/durable-streams";
1314
import type { AgentOption, AgentOptions, SessionConfiguration } from "@executablemd/core";
@@ -206,6 +207,7 @@ export interface ReplLive {
206207
* reader left it on when the next reply arrives.
207208
*/
208209
export interface ReplSidekickChatRow {
210+
readonly origin?: MessageOrigin;
209211
/**
210212
* The saved request this failed attempt could be asked again, or none.
211213
*
@@ -3250,7 +3252,13 @@ function sidekickComposedRows(
32503252
}
32513253
for (const item of shownChat) {
32523254
if (item.kind === "person") {
3253-
items.push(text(`sidekick:${item.key}`, ` you: ${item.text}`, styleOf("source")));
3255+
items.push(
3256+
text(
3257+
`sidekick:${item.key}`,
3258+
` ${item.origin?.kind === "acp" ? "ACP (" + item.origin.attachment + ")" : "you"}: ${item.text}`,
3259+
styleOf("source"),
3260+
),
3261+
);
32543262
continue;
32553263
}
32563264
if (item.kind === "preview") {

0 commit comments

Comments
 (0)