@gajae-code/coding-agent 0.16.1 → 0.16.4
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/CHANGELOG.md +70 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/cli/rows.d.ts +7 -0
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +23 -0
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/setup/paseo/announce.d.ts +111 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/index.d.ts +8 -2
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/dist/types/utils/external-editor.d.ts +5 -0
- package/package.json +7 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/root-flags.ts +5 -3
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock.ts +181 -17
- package/src/config/model-preset-registry.ts +5 -3
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +57 -11
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/discovery/builtin.ts +13 -4
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +10 -2
- package/src/extensibility/gjc-plugins/compiler.ts +208 -58
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/runtime-skill-discovery.ts +77 -151
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +44 -16
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +94 -6
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +72 -34
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +29 -14
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +393 -21
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +148 -5
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +47 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/search.md +2 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +171 -14
- package/src/runtime-mcp/pool.test.ts +209 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/acp/adapter.ts +1 -1
- package/src/sdk/broker/broker.ts +5 -4
- package/src/sdk/bus/chat-daemon-control.ts +1 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
- package/src/sdk/bus/slack-daemon.ts +92 -2
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/cli/rows.ts +47 -2
- package/src/sdk/cli/session-cli.ts +47 -23
- package/src/sdk/host/events.ts +25 -2
- package/src/sdk/host/host.ts +4 -1
- package/src/sdk/host/session-runtime.test.ts +36 -25
- package/src/sdk/host/session-runtime.ts +279 -43
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +60 -37
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +73 -30
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +123 -38
- package/src/session/contribution-prep.ts +214 -21
- package/src/setup/paseo/announce.ts +450 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +2 -1
- package/src/task/index.ts +17 -4
- package/src/tools/bash.ts +50 -21
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/index.ts +8 -2
- package/src/tools/job.ts +8 -2
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/search.ts +116 -58
- package/src/tools/skill.ts +22 -2
- package/src/tools/subagent.ts +3 -1
- package/src/tools/tool-catalog.generated.ts +9 -2
- package/src/utils/external-editor.ts +16 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Announce a live interactive session to Paseo so it appears there as a
|
|
3
|
+
* controllable ACP agent.
|
|
4
|
+
*
|
|
5
|
+
* `gjc setup paseo` registers GJC as an ACP provider, but Paseo only ever knows
|
|
6
|
+
* about the sessions it started itself. Everything else is already in place: an
|
|
7
|
+
* interactive `gjc` hosts a broker-registered SDK endpoint, and GJC's own ACP
|
|
8
|
+
* surface resolves a *live* index entry straight to that running host -- a
|
|
9
|
+
* `session/load` attaches to it instead of resuming a second copy. The one
|
|
10
|
+
* missing step is telling the Paseo daemon that the session exists, which is
|
|
11
|
+
* exactly what `paseo import` does.
|
|
12
|
+
*
|
|
13
|
+
* Two hard constraints shape this module:
|
|
14
|
+
*
|
|
15
|
+
* - The Paseo CLI blocks indefinitely when its daemon is not running (measured:
|
|
16
|
+
* no output and no exit after two minutes). It is therefore NEVER spawned
|
|
17
|
+
* before a cheap socket probe proves something is listening.
|
|
18
|
+
* - ACP `session/load` only attaches to a session the broker index reports as
|
|
19
|
+
* live; for anything else it falls back to `session.resume`, which would start
|
|
20
|
+
* a *second* host for a session that is actually running. Liveness is
|
|
21
|
+
* confirmed before the import, never assumed.
|
|
22
|
+
*
|
|
23
|
+
* Everything here is best-effort: no Paseo, a stopped daemon, or a failed import
|
|
24
|
+
* must never slow down or break an interactive launch. It is also opt-in: the
|
|
25
|
+
* caller checks `paseo.autoImport` before any of this runs, so an installation
|
|
26
|
+
* that never asked for it pays nothing at all.
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from "node:fs/promises";
|
|
29
|
+
import * as net from "node:net";
|
|
30
|
+
import * as path from "node:path";
|
|
31
|
+
import { $which, logger } from "@gajae-code/utils";
|
|
32
|
+
import { AgentDirSessionLifecycleClient } from "../../sdk/lifecycle/broker-client";
|
|
33
|
+
import { sessionListPageFromResponse, traverseSessionList } from "../../sdk/session-list";
|
|
34
|
+
import { PROVIDER_EXTENDS, PROVIDER_KEY, resolvePaseoHome } from "./setup-deps";
|
|
35
|
+
|
|
36
|
+
export { resolvePaseoHome } from "./setup-deps";
|
|
37
|
+
|
|
38
|
+
/** Cheap liveness probe of the daemon socket. Only gates the expensive CLI spawn. */
|
|
39
|
+
const DAEMON_PROBE_TIMEOUT_MS = 750;
|
|
40
|
+
/** Bound on the broker `session.list` traversal that proves our own liveness. */
|
|
41
|
+
const BROKER_QUERY_TIMEOUT_MS = 5_000;
|
|
42
|
+
/** `paseo import` spawns Electron, probes the provider, and replays history. */
|
|
43
|
+
const IMPORT_TIMEOUT_MS = 90_000;
|
|
44
|
+
/** Cadence of the retry loop while the daemon is simply not up yet. */
|
|
45
|
+
const RETRY_INTERVAL_MS = 30_000;
|
|
46
|
+
/** Retries stop after ten minutes so a launch never keeps a timer alive forever. */
|
|
47
|
+
const MAX_ATTEMPTS = 20;
|
|
48
|
+
|
|
49
|
+
/** Paseo's own default when nothing configures a listener. */
|
|
50
|
+
const DEFAULT_DAEMON_TARGET: PaseoDaemonTarget = { kind: "tcp", host: "localhost", port: 6767 };
|
|
51
|
+
|
|
52
|
+
export type PaseoDaemonTarget =
|
|
53
|
+
| { readonly kind: "ipc"; readonly socketPath: string }
|
|
54
|
+
| { readonly kind: "tcp"; readonly host: string; readonly port: number };
|
|
55
|
+
|
|
56
|
+
/** Why an announcement did nothing. Each reason is a distinct, actionable state. */
|
|
57
|
+
export type PaseoAnnounceSkip =
|
|
58
|
+
| "no-paseo-config"
|
|
59
|
+
| "no-provider"
|
|
60
|
+
| "cli-missing"
|
|
61
|
+
| "daemon-unreachable"
|
|
62
|
+
| "unsupported-daemon-target"
|
|
63
|
+
| "daemon-auth-required"
|
|
64
|
+
| "session-not-live";
|
|
65
|
+
|
|
66
|
+
export type PaseoAnnounceOutcome =
|
|
67
|
+
| { readonly kind: "imported"; readonly providerKey: string }
|
|
68
|
+
| { readonly kind: "already-imported"; readonly providerKey: string }
|
|
69
|
+
| { readonly kind: "skipped"; readonly reason: PaseoAnnounceSkip }
|
|
70
|
+
| { readonly kind: "failed"; readonly detail: string };
|
|
71
|
+
|
|
72
|
+
export interface PaseoAnnounceDependencies {
|
|
73
|
+
/** `~/.paseo/config.json` (or the `$PASEO_HOME` equivalent). */
|
|
74
|
+
readonly configJson: string;
|
|
75
|
+
/** `~/.paseo` (or `$PASEO_HOME`). */
|
|
76
|
+
readonly paseoHome: string;
|
|
77
|
+
readonly env: NodeJS.ProcessEnv;
|
|
78
|
+
/** Parsed JSON, or `undefined` when the file is absent or unreadable. */
|
|
79
|
+
readJson(file: string): Promise<unknown>;
|
|
80
|
+
/** Absolute path of the `paseo` CLI, or `undefined` when it is not installed. */
|
|
81
|
+
resolveCli(): string | undefined;
|
|
82
|
+
probeDaemon(target: PaseoDaemonTarget): Promise<boolean>;
|
|
83
|
+
/** True only when the broker index reports this session id as live in `cwd`. */
|
|
84
|
+
isSessionLive(sessionId: string, cwd: string): Promise<boolean>;
|
|
85
|
+
runImport(input: {
|
|
86
|
+
readonly cli: string;
|
|
87
|
+
readonly providerKey: string;
|
|
88
|
+
readonly cwd: string;
|
|
89
|
+
readonly sessionId: string;
|
|
90
|
+
readonly signal?: AbortSignal;
|
|
91
|
+
}): Promise<PaseoAnnounceOutcome>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
95
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pick the Paseo provider entry to import under.
|
|
100
|
+
*
|
|
101
|
+
* `gjc setup paseo` writes `gjc`, and `--mpreset <name>` writes `gjc-<name>`, so
|
|
102
|
+
* the base key is preferred and a preset provider is only used when it is the
|
|
103
|
+
* only one installed. A disabled entry is never selected: Paseo would refuse to
|
|
104
|
+
* launch it.
|
|
105
|
+
*/
|
|
106
|
+
export function selectProviderKey(config: unknown): string | undefined {
|
|
107
|
+
const providers = asRecord(asRecord(asRecord(config)?.agents)?.providers);
|
|
108
|
+
if (!providers) return undefined;
|
|
109
|
+
const usable = (key: string): boolean => {
|
|
110
|
+
const entry = asRecord(providers[key]);
|
|
111
|
+
if (entry === undefined || entry.enabled === false || entry.extends !== PROVIDER_EXTENDS) return false;
|
|
112
|
+
if (typeof entry.label !== "string" || !entry.label.startsWith("Gajae Code")) return false;
|
|
113
|
+
const command = entry.command;
|
|
114
|
+
return Array.isArray(command) && command.every(value => typeof value === "string") && command.includes("acp");
|
|
115
|
+
};
|
|
116
|
+
if (usable(PROVIDER_KEY)) return PROVIDER_KEY;
|
|
117
|
+
return Object.keys(providers)
|
|
118
|
+
.filter(key => key.startsWith(`${PROVIDER_KEY}-`) && usable(key))
|
|
119
|
+
.sort()[0];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse one Paseo listener spelling into a connectable target.
|
|
124
|
+
*
|
|
125
|
+
* Mirrors the spellings Paseo's own CLI accepts: `unix://`/`pipe://` prefixes, a
|
|
126
|
+
* bare absolute path, a bare port, `tcp://host:port`, and plain `host:port`.
|
|
127
|
+
*/
|
|
128
|
+
export function parseDaemonListen(raw: string): PaseoDaemonTarget | undefined {
|
|
129
|
+
const trimmed = raw.trim();
|
|
130
|
+
if (!trimmed) return undefined;
|
|
131
|
+
if (trimmed.startsWith("unix://")) {
|
|
132
|
+
const socketPath = trimmed.slice("unix://".length).trim();
|
|
133
|
+
return socketPath ? { kind: "ipc", socketPath } : undefined;
|
|
134
|
+
}
|
|
135
|
+
if (trimmed.startsWith("pipe://") || trimmed.startsWith("\\\\.\\pipe\\")) {
|
|
136
|
+
const socketPath = trimmed.startsWith("pipe://") ? trimmed.slice("pipe://".length).trim() : trimmed;
|
|
137
|
+
return socketPath ? { kind: "ipc", socketPath } : undefined;
|
|
138
|
+
}
|
|
139
|
+
if (trimmed.startsWith("/")) return { kind: "ipc", socketPath: trimmed };
|
|
140
|
+
if (/^\d+$/.test(trimmed)) {
|
|
141
|
+
const port = Number(trimmed);
|
|
142
|
+
return Number.isSafeInteger(port) && port >= 1 && port <= 65_535
|
|
143
|
+
? { kind: "tcp", host: "127.0.0.1", port }
|
|
144
|
+
: undefined;
|
|
145
|
+
}
|
|
146
|
+
const authority = trimmed.startsWith("tcp://") ? trimmed.slice("tcp://".length) : trimmed;
|
|
147
|
+
const withoutQuery = authority.split("?")[0] ?? "";
|
|
148
|
+
const separator = withoutQuery.lastIndexOf(":");
|
|
149
|
+
if (separator <= 0) return undefined;
|
|
150
|
+
const port = Number(withoutQuery.slice(separator + 1));
|
|
151
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) return undefined;
|
|
152
|
+
const host = withoutQuery.slice(0, separator).replace(/^\[|]$/g, "");
|
|
153
|
+
return host ? { kind: "tcp", host, port } : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve where the daemon listens, in the same precedence Paseo's CLI uses:
|
|
158
|
+
* environment override, then the running daemon's own pid file, then the
|
|
159
|
+
* configured listener, then Paseo's default.
|
|
160
|
+
*/
|
|
161
|
+
export async function resolveDaemonTarget(
|
|
162
|
+
config: unknown,
|
|
163
|
+
deps: Pick<PaseoAnnounceDependencies, "env" | "paseoHome" | "readJson">,
|
|
164
|
+
): Promise<PaseoDaemonTarget> {
|
|
165
|
+
return (await resolveDaemonTargets(config, deps))[0] ?? DEFAULT_DAEMON_TARGET;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function resolveDaemonTargets(
|
|
169
|
+
config: unknown,
|
|
170
|
+
deps: Pick<PaseoAnnounceDependencies, "env" | "paseoHome" | "readJson">,
|
|
171
|
+
): Promise<PaseoDaemonTarget[]> {
|
|
172
|
+
const explicitHost = deps.env.PASEO_HOST?.trim();
|
|
173
|
+
if (explicitHost) {
|
|
174
|
+
const explicitTarget = parseDaemonListen(explicitHost);
|
|
175
|
+
return explicitTarget ? [explicitTarget] : [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const pidFile = asRecord(await deps.readJson(path.join(deps.paseoHome, "paseo.pid")));
|
|
179
|
+
const pidListen = typeof pidFile?.listen === "string" ? pidFile.listen : pidFile?.sockPath;
|
|
180
|
+
const pidTarget = typeof pidListen === "string" ? parseDaemonListen(pidListen) : undefined;
|
|
181
|
+
|
|
182
|
+
const parsed = asRecord(config);
|
|
183
|
+
const configured = asRecord(parsed?.daemon)?.listen ?? parsed?.listen;
|
|
184
|
+
const configuredRaw = deps.env.PASEO_LISTEN?.trim() || (typeof configured === "string" ? configured : undefined);
|
|
185
|
+
const configuredTarget = configuredRaw ? parseDaemonListen(configuredRaw) : undefined;
|
|
186
|
+
const configuredIpc = configuredTarget?.kind === "ipc" ? configuredTarget : undefined;
|
|
187
|
+
const pidIpc = pidTarget?.kind === "ipc" ? pidTarget : undefined;
|
|
188
|
+
const configuredTcp =
|
|
189
|
+
configuredTarget?.kind === "tcp" && !(configuredTarget.host === "127.0.0.1" && configuredTarget.port === 6767)
|
|
190
|
+
? configuredTarget
|
|
191
|
+
: undefined;
|
|
192
|
+
const directIpc = deps.env.PASEO_LISTEN?.trim();
|
|
193
|
+
const directIpcTarget = directIpc ? parseDaemonListen(directIpc) : undefined;
|
|
194
|
+
const targets = [
|
|
195
|
+
directIpcTarget?.kind === "ipc" ? directIpcTarget : undefined,
|
|
196
|
+
pidIpc,
|
|
197
|
+
configuredIpc,
|
|
198
|
+
configuredTcp,
|
|
199
|
+
].filter((target): target is PaseoDaemonTarget => target !== undefined);
|
|
200
|
+
return targets.length > 0 ? [targets[0]!] : [DEFAULT_DAEMON_TARGET];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Turn a non-zero `paseo import` into an outcome.
|
|
205
|
+
*
|
|
206
|
+
* Two of Paseo's refusals are not failures:
|
|
207
|
+
*
|
|
208
|
+
* - an already-known session is exactly the state we wanted to reach;
|
|
209
|
+
* - a password-protected daemon (`Cannot connect to daemon ...: Password
|
|
210
|
+
* required`) is a configuration the user chose. The socket probe cannot see
|
|
211
|
+
* it -- the TCP connect succeeds -- so it can only be recognized here, and
|
|
212
|
+
* reporting it would put an error on screen at every single launch for
|
|
213
|
+
* something GJC must not fix on its own. Exporting `PASEO_PASSWORD` makes it
|
|
214
|
+
* work, since the CLI inherits this process's environment.
|
|
215
|
+
*/
|
|
216
|
+
export function classifyImportFailure(providerKey: string, detail: string): PaseoAnnounceOutcome {
|
|
217
|
+
if (/already imported/i.test(detail)) return { kind: "already-imported", providerKey };
|
|
218
|
+
if (/cannot connect to daemon[^\n]*password required/i.test(detail)) {
|
|
219
|
+
return { kind: "skipped", reason: "daemon-auth-required" };
|
|
220
|
+
}
|
|
221
|
+
return { kind: "failed", detail: "paseo import failed; inspect the Paseo daemon log" };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Run the full announcement once.
|
|
226
|
+
*
|
|
227
|
+
* The order is load-bearing: every cheap filesystem check runs before the socket
|
|
228
|
+
* probe, and the socket probe runs before anything spawns the Paseo CLI.
|
|
229
|
+
*/
|
|
230
|
+
export async function announceSessionToPaseo(
|
|
231
|
+
input: { readonly sessionId: string; readonly cwd: string },
|
|
232
|
+
deps: PaseoAnnounceDependencies,
|
|
233
|
+
signal?: AbortSignal,
|
|
234
|
+
): Promise<PaseoAnnounceOutcome> {
|
|
235
|
+
if (signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
|
|
236
|
+
const config = await deps.readJson(deps.configJson);
|
|
237
|
+
if (signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
|
|
238
|
+
if (config === undefined) return { kind: "skipped", reason: "no-paseo-config" };
|
|
239
|
+
|
|
240
|
+
const providerKey = selectProviderKey(config);
|
|
241
|
+
if (!providerKey) return { kind: "skipped", reason: "no-provider" };
|
|
242
|
+
|
|
243
|
+
const cli = deps.resolveCli();
|
|
244
|
+
if (!cli) return { kind: "skipped", reason: "cli-missing" };
|
|
245
|
+
|
|
246
|
+
const targets = await resolveDaemonTargets(config, deps);
|
|
247
|
+
if (deps.env.PASEO_HOST?.trim() && targets.length === 0)
|
|
248
|
+
return { kind: "skipped", reason: "unsupported-daemon-target" };
|
|
249
|
+
let reachable = false;
|
|
250
|
+
for (const target of targets) {
|
|
251
|
+
if (await deps.probeDaemon(target)) {
|
|
252
|
+
reachable = true;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!reachable) return { kind: "skipped", reason: "daemon-unreachable" };
|
|
257
|
+
if (signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
|
|
258
|
+
|
|
259
|
+
// Importing a session the broker does not yet report as live would make ACP
|
|
260
|
+
// `session/load` resume a copy instead of attaching to the running host.
|
|
261
|
+
if (!(await deps.isSessionLive(input.sessionId, input.cwd))) return { kind: "skipped", reason: "session-not-live" };
|
|
262
|
+
|
|
263
|
+
return await deps.runImport({ cli, providerKey, cwd: input.cwd, sessionId: input.sessionId, signal });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Only a daemon that is not up yet, or a registration that has not landed yet, is worth retrying. */
|
|
267
|
+
export function isRetryableOutcome(outcome: PaseoAnnounceOutcome): boolean {
|
|
268
|
+
return (
|
|
269
|
+
outcome.kind === "skipped" && (outcome.reason === "daemon-unreachable" || outcome.reason === "session-not-live")
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface PaseoAnnouncementHandle {
|
|
274
|
+
cancel(): void;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Announce the session, retrying only while Paseo is not ready to hear it.
|
|
279
|
+
*
|
|
280
|
+
* The user routinely opens Paseo after their terminal, so a single attempt at
|
|
281
|
+
* launch would silently never land. Retries are socket probes, not CLI spawns,
|
|
282
|
+
* and every timer is unref'd so the loop can never hold the process open.
|
|
283
|
+
*/
|
|
284
|
+
export function startPaseoAnnouncement(
|
|
285
|
+
input: {
|
|
286
|
+
readonly sessionId: string;
|
|
287
|
+
readonly cwd: string;
|
|
288
|
+
isEnabled?: () => boolean;
|
|
289
|
+
onOutcome?: (outcome: PaseoAnnounceOutcome) => void;
|
|
290
|
+
},
|
|
291
|
+
deps: PaseoAnnounceDependencies,
|
|
292
|
+
): PaseoAnnouncementHandle {
|
|
293
|
+
let cancelled = false;
|
|
294
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
|
|
297
|
+
const attempt = async (remaining: number): Promise<void> => {
|
|
298
|
+
if (cancelled || input.isEnabled?.() === false) return;
|
|
299
|
+
let outcome: PaseoAnnounceOutcome;
|
|
300
|
+
try {
|
|
301
|
+
outcome = await announceSessionToPaseo(input, deps, controller.signal);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
outcome = { kind: "failed", detail: error instanceof Error ? error.message : String(error) };
|
|
304
|
+
}
|
|
305
|
+
if (cancelled || input.isEnabled?.() === false) return;
|
|
306
|
+
if (isRetryableOutcome(outcome) && remaining > 1) {
|
|
307
|
+
// Logged per attempt: a retried outcome is otherwise completely silent,
|
|
308
|
+
// which is the exact state a user reports as "it never showed up".
|
|
309
|
+
logger.debug("Paseo session announcement retrying", { outcome, remaining: remaining - 1 });
|
|
310
|
+
timer = setTimeout(() => void attempt(remaining - 1), RETRY_INTERVAL_MS);
|
|
311
|
+
timer.unref?.();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
logger.debug("Paseo session announcement settled", { outcome });
|
|
315
|
+
try {
|
|
316
|
+
input.onOutcome?.(outcome);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
logger.debug("Paseo session announcement outcome handler failed", {
|
|
319
|
+
error: error instanceof Error ? error.message : String(error),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
void attempt(MAX_ATTEMPTS);
|
|
325
|
+
return {
|
|
326
|
+
cancel(): void {
|
|
327
|
+
cancelled = true;
|
|
328
|
+
controller.abort();
|
|
329
|
+
if (timer) clearTimeout(timer);
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function readJson(file: string): Promise<unknown> {
|
|
335
|
+
try {
|
|
336
|
+
return JSON.parse(await Bun.file(file).text());
|
|
337
|
+
} catch {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function probeDaemon(target: PaseoDaemonTarget): Promise<boolean> {
|
|
343
|
+
return new Promise(resolve => {
|
|
344
|
+
let settled = false;
|
|
345
|
+
const settle = (reachable: boolean): void => {
|
|
346
|
+
if (settled) return;
|
|
347
|
+
settled = true;
|
|
348
|
+
socket.destroy();
|
|
349
|
+
resolve(reachable);
|
|
350
|
+
};
|
|
351
|
+
const socket =
|
|
352
|
+
target.kind === "ipc"
|
|
353
|
+
? net.connect({ path: target.socketPath })
|
|
354
|
+
: net.connect({ host: target.host, port: target.port });
|
|
355
|
+
socket.setTimeout(DAEMON_PROBE_TIMEOUT_MS);
|
|
356
|
+
socket.once("connect", () => settle(true));
|
|
357
|
+
socket.once("timeout", () => settle(false));
|
|
358
|
+
socket.once("error", () => settle(false));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isSessionLive(agentDir: string): (sessionId: string, cwd: string) => Promise<boolean> {
|
|
363
|
+
return async (sessionId, cwd) => {
|
|
364
|
+
const canonicalCwd = await fs.realpath(cwd).catch(() => path.resolve(cwd));
|
|
365
|
+
const client = new AgentDirSessionLifecycleClient(agentDir);
|
|
366
|
+
const pages = await traverseSessionList(
|
|
367
|
+
{},
|
|
368
|
+
pageInput => client.global("session.list", pageInput, { timeoutMs: BROKER_QUERY_TIMEOUT_MS }),
|
|
369
|
+
response => sessionListPageFromResponse(response),
|
|
370
|
+
);
|
|
371
|
+
for (const { sessions } of pages) {
|
|
372
|
+
if (sessionListContainsLiveSessionForTests(sessions, sessionId, canonicalCwd)) return true;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function sessionListContainsLiveSessionForTests(
|
|
379
|
+
sessions: readonly unknown[],
|
|
380
|
+
sessionId: string,
|
|
381
|
+
cwd: string,
|
|
382
|
+
): boolean {
|
|
383
|
+
for (const value of sessions) {
|
|
384
|
+
const session = asRecord(value);
|
|
385
|
+
if (session?.sessionId !== sessionId || session.live !== true) continue;
|
|
386
|
+
const locatorCwd = asRecord(session.locator)?.cwd;
|
|
387
|
+
if (typeof locatorCwd === "string" && path.resolve(locatorCwd) === path.resolve(cwd)) return true;
|
|
388
|
+
}
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Run `paseo import` with the session's own environment.
|
|
394
|
+
*
|
|
395
|
+
* GJC never reads a Paseo credential and has no setting of its own for one. The
|
|
396
|
+
* CLI authenticates exactly as it would if the user ran it by hand: if
|
|
397
|
+
* `PASEO_PASSWORD` is exported it is inherited, and if it is not, a
|
|
398
|
+
* password-protected daemon refuses and the announcement becomes a quiet skip.
|
|
399
|
+
*/
|
|
400
|
+
function runImport(env: NodeJS.ProcessEnv) {
|
|
401
|
+
return async (input: {
|
|
402
|
+
readonly cli: string;
|
|
403
|
+
readonly providerKey: string;
|
|
404
|
+
readonly cwd: string;
|
|
405
|
+
readonly sessionId: string;
|
|
406
|
+
readonly signal?: AbortSignal;
|
|
407
|
+
}): Promise<PaseoAnnounceOutcome> => {
|
|
408
|
+
const timeoutController = new AbortController();
|
|
409
|
+
const signal = input.signal
|
|
410
|
+
? AbortSignal.any([input.signal, timeoutController.signal])
|
|
411
|
+
: timeoutController.signal;
|
|
412
|
+
const timer = setTimeout(() => timeoutController.abort(), IMPORT_TIMEOUT_MS);
|
|
413
|
+
try {
|
|
414
|
+
const child = Bun.spawn(
|
|
415
|
+
[input.cli, "import", "--provider", input.providerKey, "--cwd", input.cwd, input.sessionId],
|
|
416
|
+
{ stdout: "pipe", stderr: "pipe", signal, env },
|
|
417
|
+
);
|
|
418
|
+
const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
|
|
419
|
+
if (input.signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
|
|
420
|
+
if (timeoutController.signal.aborted)
|
|
421
|
+
return { kind: "failed", detail: `paseo import timed out after ${IMPORT_TIMEOUT_MS}ms` };
|
|
422
|
+
if (exitCode === 0) return { kind: "imported", providerKey: input.providerKey };
|
|
423
|
+
return classifyImportFailure(input.providerKey, stderr.trim() || `paseo import exited with ${exitCode}`);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
return { kind: "failed", detail: error instanceof Error ? error.message : String(error) };
|
|
426
|
+
} finally {
|
|
427
|
+
clearTimeout(timer);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function createDefaultPaseoAnnounceDependencies(
|
|
433
|
+
agentDir: string,
|
|
434
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
435
|
+
): PaseoAnnounceDependencies {
|
|
436
|
+
const paseoHome = resolvePaseoHome(env);
|
|
437
|
+
return {
|
|
438
|
+
configJson: path.join(paseoHome, "config.json"),
|
|
439
|
+
paseoHome,
|
|
440
|
+
env,
|
|
441
|
+
readJson,
|
|
442
|
+
// Resolve against the injected environment, not the ambient one: the env is a
|
|
443
|
+
// dependency here, and a lookup that ignores it would resolve a different
|
|
444
|
+
// executable than every other step in this module reasons about.
|
|
445
|
+
resolveCli: () => $which("paseo", env.PATH === undefined ? undefined : { PATH: env.PATH }) ?? undefined,
|
|
446
|
+
probeDaemon,
|
|
447
|
+
isSessionLive: isSessionLive(agentDir),
|
|
448
|
+
runImport: runImport(env),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
@@ -76,8 +76,20 @@ export interface PaseoSetupDependencies {
|
|
|
76
76
|
now(): Date;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export function
|
|
80
|
-
const
|
|
79
|
+
export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env, home: string = os.homedir()): string {
|
|
80
|
+
const configured = env.PASEO_HOME?.trim();
|
|
81
|
+
if (!configured) return path.join(home, ".paseo");
|
|
82
|
+
const expanded =
|
|
83
|
+
configured === "~" ? home : configured.startsWith("~/") ? path.join(home, configured.slice(2)) : configured;
|
|
84
|
+
return path.resolve(expanded);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createDefaultPaseoPaths(
|
|
88
|
+
agentDir: string = getAgentDir(),
|
|
89
|
+
home: string = os.homedir(),
|
|
90
|
+
env: NodeJS.ProcessEnv = {},
|
|
91
|
+
): PaseoPaths {
|
|
92
|
+
const paseoHome = resolvePaseoHome(env, home);
|
|
81
93
|
return {
|
|
82
94
|
configJson: path.join(paseoHome, "config.json"),
|
|
83
95
|
orchestrationPreferences: path.join(paseoHome, "orchestration-preferences.json"),
|
|
@@ -168,7 +180,7 @@ export function parseProviderLs(stdout: string): PaseoLsOutcome {
|
|
|
168
180
|
|
|
169
181
|
export function createDefaultPaseoSetupDependencies(): PaseoSetupDependencies {
|
|
170
182
|
return {
|
|
171
|
-
paths: createDefaultPaseoPaths(),
|
|
183
|
+
paths: createDefaultPaseoPaths(getAgentDir(), os.homedir(), process.env),
|
|
172
184
|
runProviderLs,
|
|
173
185
|
now: () => new Date(),
|
|
174
186
|
};
|
|
@@ -127,6 +127,7 @@ function toSlashCommandRuntime(runtime: TuiSlashCommandRuntime): SlashCommandRun
|
|
|
127
127
|
reloadPlugins: async () => {
|
|
128
128
|
const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
|
|
129
129
|
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
130
|
+
await ctx.session.reloadSkills();
|
|
130
131
|
await ctx.refreshSlashCommandState();
|
|
131
132
|
await ctx.session.refreshSshTool({ activateIfAvailable: true });
|
|
132
133
|
},
|
package/src/system-prompt.ts
CHANGED
|
@@ -368,6 +368,8 @@ export interface BuildSystemPromptOptions {
|
|
|
368
368
|
pluginAppendices?: string;
|
|
369
369
|
/** Repeat full tool descriptions in system prompt. Default: false */
|
|
370
370
|
repeatToolDescriptions?: boolean;
|
|
371
|
+
/** Language guidance for internal technical reasoning. Default: off. */
|
|
372
|
+
reasoningLanguage?: "off" | "english";
|
|
371
373
|
/** Skills settings for discovery. */
|
|
372
374
|
skillsSettings?: SkillsSettings;
|
|
373
375
|
/** Working directory. Default: getProjectDir() */
|
|
@@ -542,6 +544,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
542
544
|
appendSystemPrompt,
|
|
543
545
|
pluginAppendices,
|
|
544
546
|
repeatToolDescriptions = false,
|
|
547
|
+
reasoningLanguage = "off",
|
|
545
548
|
toolNames: providedToolNames,
|
|
546
549
|
cwd,
|
|
547
550
|
contextFiles: providedContextFiles,
|
|
@@ -710,6 +713,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
710
713
|
tools: toolNames,
|
|
711
714
|
toolInfo,
|
|
712
715
|
repeatToolDescriptions,
|
|
716
|
+
reasoningLanguageEnglish: reasoningLanguage === "english",
|
|
713
717
|
toolRefs,
|
|
714
718
|
environment,
|
|
715
719
|
contextFiles: sanitizedContextFiles,
|
package/src/task/executor.ts
CHANGED
|
@@ -300,7 +300,8 @@ export interface ExecutorOptions {
|
|
|
300
300
|
managedPersistence?: ManagedTaskPersistence;
|
|
301
301
|
/**
|
|
302
302
|
* The parent session's ENDPOINT-owned AsyncJobManager (resolved by the
|
|
303
|
-
* TaskTool via
|
|
303
|
+
* TaskTool via its opaque async endpoint accessor, then the process-global
|
|
304
|
+
* fallback). Model metadata and
|
|
304
305
|
* live-handle state for THIS subagent are recorded in the SAME manager the
|
|
305
306
|
* task job runs in — with concurrent top-level sessions the process-global
|
|
306
307
|
* instance belongs to a different session and would surface this subagent
|
package/src/task/index.ts
CHANGED
|
@@ -375,6 +375,14 @@ function createTaskModeError(text: string): AgentToolResult<TaskToolDetails> {
|
|
|
375
375
|
};
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
/** Remove only strict-provider blank placeholders for the optional caller-authored schema. */
|
|
379
|
+
function normalizeTaskAdmissionParams(params: TaskParams): TaskParams {
|
|
380
|
+
if (typeof params.schema !== "string" || params.schema.trim() !== "") return params;
|
|
381
|
+
const normalized = { ...params };
|
|
382
|
+
delete normalized.schema;
|
|
383
|
+
return normalized;
|
|
384
|
+
}
|
|
385
|
+
|
|
378
386
|
function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams): string | undefined {
|
|
379
387
|
const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
|
|
380
388
|
const disallowedFields: string[] = [];
|
|
@@ -888,7 +896,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
888
896
|
* thread P1).
|
|
889
897
|
*/
|
|
890
898
|
#resolveOwnedJobManager(): AsyncJobManager | undefined {
|
|
891
|
-
const endpointId = this.session.getSessionId?.() ?? undefined;
|
|
899
|
+
const endpointId = this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined;
|
|
892
900
|
return (
|
|
893
901
|
this.session.getAsyncJobManager?.() ?? AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance()
|
|
894
902
|
);
|
|
@@ -900,7 +908,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
900
908
|
signal?: AbortSignal,
|
|
901
909
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
902
910
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
903
|
-
const params = rawParams as TaskParams;
|
|
911
|
+
const params = normalizeTaskAdmissionParams(rawParams as TaskParams);
|
|
904
912
|
const simpleMode = this.#getTaskSimpleMode();
|
|
905
913
|
const validationError = validateTaskModeParams(simpleMode, params);
|
|
906
914
|
if (validationError) {
|
|
@@ -1245,7 +1253,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1245
1253
|
manager,
|
|
1246
1254
|
resumeToolCallId ?? descriptor.toolCallId,
|
|
1247
1255
|
resumeJobId,
|
|
1248
|
-
this.session.getSessionId?.() ?? undefined,
|
|
1256
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
1249
1257
|
);
|
|
1250
1258
|
return resumeJobId;
|
|
1251
1259
|
};
|
|
@@ -1477,7 +1485,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1477
1485
|
},
|
|
1478
1486
|
},
|
|
1479
1487
|
);
|
|
1480
|
-
registerOwnedIfLineaged(
|
|
1488
|
+
registerOwnedIfLineaged(
|
|
1489
|
+
manager,
|
|
1490
|
+
_toolCallId,
|
|
1491
|
+
jobId,
|
|
1492
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
1493
|
+
);
|
|
1481
1494
|
startedJobs.push({ jobId, taskId: taskItem.id });
|
|
1482
1495
|
if (typeof manager.registerResumeDescriptor === "function") {
|
|
1483
1496
|
manager.registerResumeDescriptor(
|