agent-sh 0.14.11 → 0.15.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/README.md +38 -42
- package/dist/agent/agent-loop.d.ts +9 -17
- package/dist/agent/agent-loop.js +104 -136
- package/dist/agent/events.d.ts +8 -11
- package/dist/agent/host-types.d.ts +17 -11
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +38 -22
- package/dist/agent/providers/deepseek.js +9 -1
- package/dist/agent/session-store.js +1 -1
- package/dist/agent/system-prompt.d.ts +7 -3
- package/dist/agent/system-prompt.js +11 -14
- package/dist/agent/tool-protocol.js +0 -7
- package/dist/cli/args.js +2 -1
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.js +29 -1
- package/dist/cli/subcommands.js +1 -0
- package/dist/core/event-bus.js +0 -2
- package/dist/core/extension-loader.js +3 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +3 -2
- package/dist/extensions/slash-commands/index.js +16 -11
- package/dist/shell/index.js +9 -0
- package/dist/shell/shell-context.d.ts +2 -2
- package/dist/shell/shell-context.js +26 -11
- package/dist/shell/tui-renderer.js +0 -1
- package/dist/utils/diff-renderer.js +2 -9
- package/dist/utils/handler-registry.d.ts +1 -6
- package/dist/utils/handler-registry.js +1 -6
- package/dist/utils/line-editor.js +0 -2
- package/dist/utils/palette.js +4 -4
- package/dist/utils/terminal-buffer.d.ts +2 -0
- package/dist/utils/terminal-buffer.js +4 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +11 -7
- package/examples/extensions/ash-scheme/index.ts +104 -74
- package/examples/extensions/ashi/EXTENDING.md +2 -0
- package/examples/extensions/ashi/README.md +17 -1
- package/examples/extensions/ashi/docs/ui-surface-protocol.md +163 -0
- package/examples/extensions/ashi/package.json +9 -1
- package/examples/extensions/ashi/src/capture.ts +45 -7
- package/examples/extensions/ashi/src/chat/assistant.ts +23 -43
- package/examples/extensions/ashi/src/chat/lines.ts +20 -1
- package/examples/extensions/ashi/src/cli.ts +25 -3
- package/examples/extensions/ashi/src/clipboard-image.ts +1 -1
- package/examples/extensions/ashi/src/dialogs.ts +67 -0
- package/examples/extensions/ashi/src/display-config.ts +7 -0
- package/examples/extensions/ashi/src/docks.ts +31 -0
- package/examples/extensions/ashi/src/events.ts +16 -0
- package/examples/extensions/ashi/src/frontend.ts +134 -27
- package/examples/extensions/ashi/src/hooks.ts +6 -12
- package/examples/extensions/ashi/src/input-prompt.ts +64 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/index.ts +7 -3
- package/examples/extensions/ashi/src/renderers/pi-tui/nodes.ts +67 -10
- package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +11 -1
- package/examples/extensions/ashi/src/schema.ts +3 -0
- package/examples/extensions/ashi/src/session-commands.ts +2 -1
- package/examples/extensions/ashi/src/status-footer.ts +21 -3
- package/examples/extensions/ashi/src/ui.ts +88 -0
- package/examples/extensions/ashi-ink/README.md +2 -0
- package/examples/extensions/ashi-scheme-render.ts +8 -2
- package/examples/extensions/ashi-ui-demo.ts +63 -0
- package/examples/extensions/latex-images.ts +57 -9
- package/examples/extensions/overlay-agent.ts +5 -5
- package/examples/extensions/pi-bridge/index.ts +7 -12
- package/package.json +1 -1
|
@@ -56,19 +56,16 @@ export interface ProviderRegistration {
|
|
|
56
56
|
/** Local daemons etc. — `auth list/login` shows "no auth required". */
|
|
57
57
|
noAuth?: boolean;
|
|
58
58
|
}
|
|
59
|
-
/** A model
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
apiKey: string;
|
|
67
|
-
baseURL?: string;
|
|
68
|
-
};
|
|
59
|
+
/** A selectable (provider, model) target the frontend lists and switches.
|
|
60
|
+
* Serializable — identity + capabilities only; the secret + closures needed to
|
|
61
|
+
* invoke it live in ModelEndpoint, so this can safely cross to frontends and
|
|
62
|
+
* out-of-process bridges. */
|
|
63
|
+
export interface Model {
|
|
64
|
+
id: string;
|
|
65
|
+
provider: string;
|
|
69
66
|
/** Context window size in tokens (for usage display). */
|
|
70
67
|
contextWindow?: number;
|
|
71
|
-
/** Max output tokens
|
|
68
|
+
/** Max output tokens. */
|
|
72
69
|
maxTokens?: number;
|
|
73
70
|
/** Model supports reasoning/thinking tokens. */
|
|
74
71
|
reasoning?: boolean;
|
|
@@ -79,7 +76,15 @@ export interface AgentMode {
|
|
|
79
76
|
echoReasoning?: boolean;
|
|
80
77
|
/** Input modalities the model supports. Defaults to ["text"]. */
|
|
81
78
|
modalities?: ("text" | "image")[];
|
|
79
|
+
}
|
|
80
|
+
/** Credentials + provider-shape transforms for invoking a Model, resolved by
|
|
81
|
+
* (provider, id). Internal: holds a secret (apiKey) and non-serializable
|
|
82
|
+
* closures, so it must never ride a bus event. */
|
|
83
|
+
export interface ModelEndpoint {
|
|
84
|
+
apiKey: string;
|
|
85
|
+
baseURL?: string;
|
|
82
86
|
buildReasoningParams?: (level: string) => Record<string, unknown>;
|
|
87
|
+
extractCachedTokens?: (usage: Record<string, unknown>) => number | undefined;
|
|
83
88
|
}
|
|
84
89
|
/**
|
|
85
90
|
* Capabilities the agent host adds on top of CoreContext. Only available
|
|
@@ -94,6 +99,7 @@ export interface AgentSurface {
|
|
|
94
99
|
unregister: (id: string) => void;
|
|
95
100
|
configure: (id: string, opts: {
|
|
96
101
|
reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
|
|
102
|
+
cacheTokens?: (usage: Record<string, unknown>) => number | undefined;
|
|
97
103
|
}) => void;
|
|
98
104
|
};
|
|
99
105
|
registerTool: (tool: ToolDefinition) => void;
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export { AgentLoop } from "./agent-loop.js";
|
|
|
11
11
|
export { ToolRegistry } from "./tool-registry.js";
|
|
12
12
|
export { runSubagent, type SubagentOptions } from "./subagent.js";
|
|
13
13
|
/** Built-in providers register unconditionally so `auth list` can
|
|
14
|
-
* enumerate them;
|
|
14
|
+
* enumerate them; buildModels() skips entries without an apiKey. */
|
|
15
15
|
export declare function activateAgent(ctx: ExtensionContext): void;
|
package/dist/agent/index.js
CHANGED
|
@@ -43,6 +43,10 @@ function defaultReasoningBuilder(level) {
|
|
|
43
43
|
return {};
|
|
44
44
|
return { reasoning_effort: level === "xhigh" ? "high" : level };
|
|
45
45
|
}
|
|
46
|
+
function defaultCacheTokens(usage) {
|
|
47
|
+
const details = usage.prompt_tokens_details;
|
|
48
|
+
return typeof details?.cached_tokens === "number" ? details.cached_tokens : undefined;
|
|
49
|
+
}
|
|
46
50
|
function mergeCaps(settingsCaps, payloadCaps, modelIds) {
|
|
47
51
|
if (!settingsCaps)
|
|
48
52
|
return payloadCaps.size > 0 ? payloadCaps : undefined;
|
|
@@ -91,11 +95,12 @@ export default function agentBackend(ctx) {
|
|
|
91
95
|
settingsProviders.set(name, p);
|
|
92
96
|
}
|
|
93
97
|
const providerHooks = new Map();
|
|
94
|
-
// Bakes model id so
|
|
98
|
+
// Bakes model id so ModelEndpoint.buildReasoningParams keeps its (level) signature.
|
|
95
99
|
const bindReasoning = (shapeId, model) => {
|
|
96
100
|
const hook = providerHooks.get(shapeId)?.reasoningParams;
|
|
97
101
|
return hook ? (level) => hook(level, model) : defaultReasoningBuilder;
|
|
98
102
|
};
|
|
103
|
+
const bindCacheTokens = (shapeId) => providerHooks.get(shapeId)?.cacheTokens ?? defaultCacheTokens;
|
|
99
104
|
const agentSurface = {
|
|
100
105
|
llm: createLlmFacade({ list: ctx.list, call: ctx.call }),
|
|
101
106
|
providers: {
|
|
@@ -280,33 +285,43 @@ export default function agentBackend(ctx) {
|
|
|
280
285
|
}
|
|
281
286
|
return out;
|
|
282
287
|
};
|
|
283
|
-
const
|
|
288
|
+
const buildModels = () => {
|
|
284
289
|
const out = [];
|
|
285
290
|
for (const [id, p] of resolvedProviders) {
|
|
286
291
|
if (!p.apiKey)
|
|
287
292
|
continue;
|
|
288
293
|
if (!usableProvider(p))
|
|
289
294
|
continue;
|
|
290
|
-
const shapeId = p.reasoningShape ?? id;
|
|
291
295
|
for (const model of p.models) {
|
|
292
296
|
const mc = p.modelCapabilities?.get(model);
|
|
293
297
|
out.push({
|
|
294
|
-
model,
|
|
298
|
+
id: model,
|
|
295
299
|
provider: id,
|
|
296
|
-
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
297
300
|
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
298
301
|
maxTokens: mc?.maxTokens ?? (mc?.contextWindow ? Math.min(Math.floor(mc.contextWindow * 0.4), 65536) : undefined),
|
|
299
302
|
reasoning: mc?.reasoning,
|
|
300
303
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
301
304
|
echoReasoning: mc?.echoReasoning,
|
|
302
305
|
modalities: mc?.modalities,
|
|
303
|
-
buildReasoningParams: bindReasoning(shapeId, model),
|
|
304
306
|
});
|
|
305
307
|
}
|
|
306
308
|
}
|
|
307
309
|
return out;
|
|
308
310
|
};
|
|
309
|
-
|
|
311
|
+
const resolveEndpoint = (providerId, modelId) => {
|
|
312
|
+
const p = resolvedProviders.get(providerId);
|
|
313
|
+
if (!p?.apiKey)
|
|
314
|
+
return undefined;
|
|
315
|
+
const shapeId = p.reasoningShape ?? providerId;
|
|
316
|
+
return {
|
|
317
|
+
apiKey: p.apiKey,
|
|
318
|
+
baseURL: p.baseURL,
|
|
319
|
+
buildReasoningParams: bindReasoning(shapeId, modelId),
|
|
320
|
+
extractCachedTokens: bindCacheTokens(shapeId),
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
ctx.define("agent:get-models", () => buildModels());
|
|
324
|
+
ctx.define("agent:resolve-endpoint", ({ provider, id }) => resolveEndpoint(provider, id));
|
|
310
325
|
// Reconfigured at core:extensions-loaded; start() gates on `resolved`.
|
|
311
326
|
const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
|
|
312
327
|
ctx.define("llm:get-client", () => llmClient);
|
|
@@ -329,10 +344,10 @@ export default function agentBackend(ctx) {
|
|
|
329
344
|
resolvedProviders = computeResolvedProviders();
|
|
330
345
|
if (!resolved)
|
|
331
346
|
return;
|
|
332
|
-
bus.emit("agent:
|
|
347
|
+
bus.emit("agent:models-changed", {});
|
|
333
348
|
if (!ashActive)
|
|
334
349
|
return;
|
|
335
|
-
if (
|
|
350
|
+
if (buildModels().some((m) => m.id === llmClient.model))
|
|
336
351
|
return;
|
|
337
352
|
const pendingProvider = getSettings().defaultProvider;
|
|
338
353
|
if (!pendingProvider)
|
|
@@ -342,13 +357,15 @@ export default function agentBackend(ctx) {
|
|
|
342
357
|
return;
|
|
343
358
|
const pendingModel = persistedModelFor(pendingProvider);
|
|
344
359
|
if (pendingModel && p.models.includes(pendingModel) && llmClient.model !== pendingModel) {
|
|
345
|
-
bus.emit("config:switch-model", {
|
|
360
|
+
bus.emit("config:switch-model", { id: pendingModel, provider: pendingProvider });
|
|
346
361
|
}
|
|
347
362
|
});
|
|
348
|
-
bus.on("provider:configure", ({ id, reasoningParams }) => {
|
|
363
|
+
bus.on("provider:configure", ({ id, reasoningParams, cacheTokens }) => {
|
|
349
364
|
const prev = providerHooks.get(id) ?? {};
|
|
350
365
|
if (reasoningParams !== undefined)
|
|
351
366
|
prev.reasoningParams = reasoningParams;
|
|
367
|
+
if (cacheTokens !== undefined)
|
|
368
|
+
prev.cacheTokens = cacheTokens;
|
|
352
369
|
providerHooks.set(id, prev);
|
|
353
370
|
});
|
|
354
371
|
bus.on("core:extensions-loaded", ({ names }) => {
|
|
@@ -388,15 +405,14 @@ export default function agentBackend(ctx) {
|
|
|
388
405
|
// No provider → don't register ash; let another backend own activation.
|
|
389
406
|
if (!effectiveApiKey || !effectiveModel)
|
|
390
407
|
return;
|
|
391
|
-
const
|
|
408
|
+
const foundModel = buildModels().find((m) => m.id === effectiveModel && (!activeProvider || m.provider === activeProvider.id));
|
|
392
409
|
// Stub when openrouter's async catalog hasn't returned yet; reconciled
|
|
393
410
|
// later via agent:providers:changed → config:switch-model.
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
provider: activeProvider
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} : { model: effectiveModel });
|
|
411
|
+
const initialModel = foundModel ?? {
|
|
412
|
+
id: effectiveModel,
|
|
413
|
+
provider: activeProvider?.id ?? providerName ?? "custom",
|
|
414
|
+
supportsReasoningEffort: activeProvider?.supportsReasoningEffort,
|
|
415
|
+
};
|
|
400
416
|
llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
|
|
401
417
|
resolved = true;
|
|
402
418
|
bus.emit("agent:register-backend", {
|
|
@@ -413,7 +429,7 @@ export default function agentBackend(ctx) {
|
|
|
413
429
|
bus,
|
|
414
430
|
llmClient,
|
|
415
431
|
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
|
|
416
|
-
|
|
432
|
+
initialModel,
|
|
417
433
|
compositor: ctx.shell?.compositor,
|
|
418
434
|
instanceId: ctx.instanceId,
|
|
419
435
|
});
|
|
@@ -505,8 +521,8 @@ export default function agentBackend(ctx) {
|
|
|
505
521
|
return;
|
|
506
522
|
}
|
|
507
523
|
llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
|
|
508
|
-
bus.emit("agent:
|
|
509
|
-
bus.emit("config:switch-model", {
|
|
524
|
+
bus.emit("agent:models-changed", {});
|
|
525
|
+
bus.emit("config:switch-model", { id: switchModel, provider: name });
|
|
510
526
|
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
511
527
|
});
|
|
512
528
|
bus.onPipe("banner:collect", (e) => {
|
|
@@ -526,7 +542,7 @@ export { AgentLoop } from "./agent-loop.js";
|
|
|
526
542
|
export { ToolRegistry } from "./tool-registry.js";
|
|
527
543
|
export { runSubagent } from "./subagent.js";
|
|
528
544
|
/** Built-in providers register unconditionally so `auth list` can
|
|
529
|
-
* enumerate them;
|
|
545
|
+
* enumerate them; buildModels() skips entries without an apiKey. */
|
|
530
546
|
export function activateAgent(ctx) {
|
|
531
547
|
agentBackend(ctx);
|
|
532
548
|
const agentCtx = ctx;
|
|
@@ -10,7 +10,15 @@ function buildReasoningParams(level, _model) {
|
|
|
10
10
|
: { thinking: { type: "enabled" }, reasoning_effort: level };
|
|
11
11
|
}
|
|
12
12
|
export default function activate(ctx) {
|
|
13
|
-
ctx.agent.providers.configure("deepseek", {
|
|
13
|
+
ctx.agent.providers.configure("deepseek", {
|
|
14
|
+
reasoningParams: buildReasoningParams,
|
|
15
|
+
// Native DeepSeek reports caching as flat hit/miss counts, not the
|
|
16
|
+
// OpenAI-standard prompt_tokens_details.cached_tokens the default reads.
|
|
17
|
+
cacheTokens: (u) => {
|
|
18
|
+
const hit = u.prompt_cache_hit_tokens;
|
|
19
|
+
return typeof hit === "number" ? hit : undefined;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
14
22
|
ctx.agent.providers.register({
|
|
15
23
|
id: "deepseek",
|
|
16
24
|
apiKey: resolveApiKey("deepseek").key ?? undefined,
|
|
@@ -237,7 +237,7 @@ export class SessionStore {
|
|
|
237
237
|
for (const e of this.entries.values()) {
|
|
238
238
|
if (e.type === "message" && e.message.role === "user") {
|
|
239
239
|
const raw = typeof e.message.content === "string" ? e.message.content : "";
|
|
240
|
-
const txt = stripContextWrappers(raw);
|
|
240
|
+
const txt = stripContextWrappers(raw).replace(/\s+/g, " ").trim();
|
|
241
241
|
if (txt)
|
|
242
242
|
return txt.slice(0, 80);
|
|
243
243
|
}
|
|
@@ -7,10 +7,14 @@ import { type Skill } from "./skills.js";
|
|
|
7
7
|
export declare function formatSkillsBlock(skills: Skill[]): string;
|
|
8
8
|
export declare function loadGlobalAgentsMd(): string | null;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
* Contains only identity and behavioral instructions.
|
|
10
|
+
* Identity — paragraph one of the system prompt. Surface-agnostic, cacheable.
|
|
12
11
|
*/
|
|
13
|
-
export declare const
|
|
12
|
+
export declare const STATIC_IDENTITY = "You are ash, an AI coding assistant running inside agent-sh \u2014 a composable agent runtime with a small core and everything else, including the frontend you're attached to, layered on as extensions.";
|
|
13
|
+
/**
|
|
14
|
+
* The rest of the static prompt — code map, tool guidance, envelope contract.
|
|
15
|
+
* Follows the frontend surface description in the assembled prompt.
|
|
16
|
+
*/
|
|
17
|
+
export declare const STATIC_GUIDE: string;
|
|
14
18
|
/**
|
|
15
19
|
* CWD-scoped static context: project conventions (CLAUDE.md / AGENT.md)
|
|
16
20
|
* and discovered skills. Stable for a given cwd — callers should cache
|
|
@@ -85,14 +85,14 @@ function loadConventionFiles(dir) {
|
|
|
85
85
|
return result;
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
89
|
-
* Contains only identity and behavioral instructions.
|
|
88
|
+
* Identity — paragraph one of the system prompt. Surface-agnostic, cacheable.
|
|
90
89
|
*/
|
|
91
|
-
export const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
export const STATIC_IDENTITY = `You are ash, an AI coding assistant running inside agent-sh — a composable agent runtime with a small core and everything else, including the frontend you're attached to, layered on as extensions.`;
|
|
91
|
+
/**
|
|
92
|
+
* The rest of the static prompt — code map, tool guidance, envelope contract.
|
|
93
|
+
* Follows the frontend surface description in the assembled prompt.
|
|
94
|
+
*/
|
|
95
|
+
export const STATIC_GUIDE = `agent-sh source and documentation live at ${CODE_DIR}. Read them when you need to understand how the runtime works, or when the user asks how to modify or extend it:
|
|
96
96
|
- ${path.join(CODE_DIR, "docs")} — start with README.md; architecture.md and extensions.md cover the kernel boundary and extension API
|
|
97
97
|
- ${path.join(CODE_DIR, "src")} — kernel in src/core, default backend in src/agent, shell host in src/shell, built-in extensions in src/extensions
|
|
98
98
|
- ${path.join(CODE_DIR, "examples/extensions")} — reference extensions to study or copy when adding functionality
|
|
@@ -105,15 +105,12 @@ guidance rather than assuming a particular tool exists. Tool output is
|
|
|
105
105
|
returned to you for reasoning — the user doesn't see it directly.
|
|
106
106
|
|
|
107
107
|
# Context Envelopes
|
|
108
|
-
- \`<query_context>\` (contains \`<cwd>\` always, and \`<shell_events>\` when there were user shell commands since the last turn): the user's situation when they sent this turn — \`<cwd>\` anchors where they are right now, \`<shell_events>\` grounds "fix this" / "what just happened" requests. Trust the most recent \`<cwd>\` over any cwd referenced in earlier history.
|
|
109
|
-
- \`<dynamic_context>\`: current system state — in-flight work, mode markers, warnings.
|
|
110
|
-
\`<dynamic_context>\` may be absent on any turn.
|
|
111
108
|
|
|
112
|
-
|
|
109
|
+
A turn may be preceded by either of two wrappers:
|
|
110
|
+
- \`<query_context>\`: the user's situation when they sent this turn — the frontend and extensions inject what grounds the request here. Trust the most recent values over anything referenced earlier in history.
|
|
111
|
+
- \`<dynamic_context>\`: current system state — in-flight work, mode markers, warnings.
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
and conversation context for recurring patterns — apply them proactively and do not wait to
|
|
116
|
-
be reminded.`;
|
|
113
|
+
Either may be absent on any turn.`;
|
|
117
114
|
/**
|
|
118
115
|
* CWD-scoped static context: project conventions (CLAUDE.md / AGENT.md)
|
|
119
116
|
* and discovered skills. Stable for a given cwd — callers should cache
|
|
@@ -84,7 +84,6 @@ export class InlineToolProtocol {
|
|
|
84
84
|
const name = obj.tool;
|
|
85
85
|
if (typeof name !== "string")
|
|
86
86
|
continue;
|
|
87
|
-
// Separate tool name from args
|
|
88
87
|
const { tool: _, ...args } = obj;
|
|
89
88
|
calls.push({
|
|
90
89
|
id: `inline_${++this.callCounter}`,
|
|
@@ -128,7 +127,6 @@ class CodeBlockFilter {
|
|
|
128
127
|
let raw = "";
|
|
129
128
|
while (this.buf.length > 0) {
|
|
130
129
|
if (this.inFence) {
|
|
131
|
-
// Look for closing ```
|
|
132
130
|
const closeIdx = this.buf.indexOf("```");
|
|
133
131
|
if (closeIdx !== -1) {
|
|
134
132
|
// Skip past closing ``` and any trailing whitespace on that line
|
|
@@ -142,7 +140,6 @@ class CodeBlockFilter {
|
|
|
142
140
|
// No closing yet — keep buffering
|
|
143
141
|
break;
|
|
144
142
|
}
|
|
145
|
-
// Look for opening ```tool
|
|
146
143
|
const openIdx = this.buf.indexOf("```tool");
|
|
147
144
|
if (openIdx !== -1) {
|
|
148
145
|
// Emit everything before the fence, trimming trailing newline
|
|
@@ -184,7 +181,6 @@ class CodeBlockFilter {
|
|
|
184
181
|
raw += this.buf;
|
|
185
182
|
this.buf = "";
|
|
186
183
|
}
|
|
187
|
-
// Collapse runs of 3+ newlines into 2 (one blank line max)
|
|
188
184
|
return this.collapseNewlines(raw);
|
|
189
185
|
}
|
|
190
186
|
flush() {
|
|
@@ -209,7 +205,6 @@ class CodeBlockFilter {
|
|
|
209
205
|
prefix = "\n".repeat(Math.min(leading, allowed));
|
|
210
206
|
text = text.slice(leading);
|
|
211
207
|
}
|
|
212
|
-
// Collapse internal runs
|
|
213
208
|
text = text.replace(/\n{3,}/g, "\n\n");
|
|
214
209
|
// Track trailing newlines for next call
|
|
215
210
|
let trailing = 0;
|
|
@@ -322,9 +317,7 @@ export class DeferredToolProtocol {
|
|
|
322
317
|
if (schemaProps) {
|
|
323
318
|
const validParams = new Set(Object.keys(schemaProps));
|
|
324
319
|
const providedParams = Object.keys(targetArgs);
|
|
325
|
-
// Check for unknown params (likely wrong names)
|
|
326
320
|
const unknown = providedParams.filter((p) => !validParams.has(p));
|
|
327
|
-
// Check for missing required params
|
|
328
321
|
const missing = [...requiredParams].filter((p) => !targetArgs[p]);
|
|
329
322
|
if (unknown.length > 0 || missing.length > 0) {
|
|
330
323
|
const expected = [...validParams]
|
package/dist/cli/args.js
CHANGED
|
@@ -3,9 +3,10 @@ const HELP_TEXT = `agent-sh — a shell-first terminal where AI is one keystroke
|
|
|
3
3
|
|
|
4
4
|
Usage: agent-sh [options]
|
|
5
5
|
agent-sh init [--force] Scaffold ~/.agent-sh/ (settings, examples, AGENTS.md)
|
|
6
|
-
agent-sh install <spec> [--force] [--sync-deps]
|
|
6
|
+
agent-sh install <spec> [--force] [--sync-deps] [--dev]
|
|
7
7
|
Install an extension (bundled name, file:, npm:, github:)
|
|
8
8
|
--sync-deps rewrites a stale agent-sh pin to the host version
|
|
9
|
+
--dev links the extension against the running host's core (local development)
|
|
9
10
|
agent-sh uninstall <name> Remove an installed extension
|
|
10
11
|
agent-sh list List installed extensions
|
|
11
12
|
agent-sh auth login [provider] Store an API key for a built-in provider
|
package/dist/cli/install.d.ts
CHANGED
package/dist/cli/install.js
CHANGED
|
@@ -190,6 +190,32 @@ function rewriteFileDeps(target, sourcePath) {
|
|
|
190
190
|
if (changed)
|
|
191
191
|
fs.writeFileSync(pkgJson, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
192
192
|
}
|
|
193
|
+
/** --dev: repoint the extension's agent-sh dep at the running host's package
|
|
194
|
+
* root, so the install builds and runs against the local (possibly unreleased)
|
|
195
|
+
* core instead of the published registry version. npm links the file: path, so
|
|
196
|
+
* later core rebuilds flow through without reinstalling. */
|
|
197
|
+
function pinHostCore(target) {
|
|
198
|
+
const pkgJson = path.join(target, "package.json");
|
|
199
|
+
if (!fs.existsSync(pkgJson))
|
|
200
|
+
return;
|
|
201
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
|
|
202
|
+
const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
203
|
+
let changed = false;
|
|
204
|
+
for (const section of sections) {
|
|
205
|
+
const deps = pkg[section];
|
|
206
|
+
if (!deps || typeof deps !== "object")
|
|
207
|
+
continue;
|
|
208
|
+
const d = deps;
|
|
209
|
+
if (typeof d["agent-sh"] !== "string")
|
|
210
|
+
continue;
|
|
211
|
+
d["agent-sh"] = `file:${PACKAGE_ROOT}`;
|
|
212
|
+
changed = true;
|
|
213
|
+
}
|
|
214
|
+
if (!changed)
|
|
215
|
+
return;
|
|
216
|
+
fs.writeFileSync(pkgJson, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
217
|
+
console.log(`agent-sh: --dev — linking ${path.basename(target)} against host core at ${PACKAGE_ROOT}`);
|
|
218
|
+
}
|
|
193
219
|
function maybeNpmInstall(target, pkg) {
|
|
194
220
|
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) };
|
|
195
221
|
if (Object.keys(deps).length === 0)
|
|
@@ -252,7 +278,7 @@ function linkBins(target, pkg) {
|
|
|
252
278
|
}
|
|
253
279
|
export async function runInstall(spec, opts = {}) {
|
|
254
280
|
if (!spec) {
|
|
255
|
-
console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force] [--sync-deps]\n\n" +
|
|
281
|
+
console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force] [--sync-deps] [--dev]\n\n" +
|
|
256
282
|
"Bundled extensions:\n" +
|
|
257
283
|
listBundled()
|
|
258
284
|
.map((n) => ` ${n}`)
|
|
@@ -286,6 +312,8 @@ export async function runInstall(spec, opts = {}) {
|
|
|
286
312
|
});
|
|
287
313
|
try {
|
|
288
314
|
rewriteFileDeps(target, resolved.sourcePath);
|
|
315
|
+
if (opts.dev)
|
|
316
|
+
pinHostCore(target);
|
|
289
317
|
syncAgentShVersion(target, opts.syncDeps ?? false);
|
|
290
318
|
const pkg = readPackageJson(target);
|
|
291
319
|
if (pkg) {
|
package/dist/cli/subcommands.js
CHANGED
package/dist/core/event-bus.js
CHANGED
|
@@ -159,9 +159,7 @@ export class EventBus {
|
|
|
159
159
|
* returns the original payload unchanged (with safe defaults).
|
|
160
160
|
*/
|
|
161
161
|
async emitPipeAsync(event, payload) {
|
|
162
|
-
// Phase 1: notify (lets renderers prepare for interactive I/O)
|
|
163
162
|
this.dispatch(event, payload);
|
|
164
|
-
// Phase 2: transform (extensions provide decisions)
|
|
165
163
|
const listeners = this.asyncPipeListeners.get(event);
|
|
166
164
|
if (!listeners)
|
|
167
165
|
return payload;
|
|
@@ -119,7 +119,9 @@ function createScopedContext(ctx, extensionName) {
|
|
|
119
119
|
onDispose: (fn) => { cleanups.push(fn); },
|
|
120
120
|
};
|
|
121
121
|
const dispose = () => {
|
|
122
|
-
|
|
122
|
+
// Snapshot: a re-registering cleanup appends a new cleanup, and iterating
|
|
123
|
+
// the live array would run it and undo the restore in the same pass.
|
|
124
|
+
for (const fn of cleanups.slice()) {
|
|
123
125
|
try {
|
|
124
126
|
fn();
|
|
125
127
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { HandlerRegistry } from "../utils/handler-registry.js";
|
|
|
13
13
|
export { EventBus } from "./event-bus.js";
|
|
14
14
|
export type { BusEvents, ContentBlock, BackendRegistration } from "./event-bus.js";
|
|
15
15
|
export type { CoreContext, CoreConfig } from "./types.js";
|
|
16
|
-
export type { AgentContext, AgentConfig, AgentSurface, AgentConfigSurface,
|
|
16
|
+
export type { AgentContext, AgentConfig, AgentSurface, AgentConfigSurface, Model, LlmInterface, LlmMessage, LlmSession } from "../agent/host-types.js";
|
|
17
17
|
export type { ShellContext, ShellConfig, ShellSurface, ShellConfigSurface, ExtensionContext, RemoteSession, RemoteSessionOptions, RenderSurface, InputModeConfig, TerminalSession, BlockTransformOptions, FencedBlockTransformOptions, AppConfig } from "../shell/host-types.js";
|
|
18
18
|
export { palette, setPalette, resetPalette } from "../utils/palette.js";
|
|
19
19
|
export type { ColorPalette } from "../utils/palette.js";
|
package/dist/core/index.js
CHANGED
|
@@ -29,10 +29,11 @@ export function createCore(config) {
|
|
|
29
29
|
bus.setSource(instanceId);
|
|
30
30
|
handlers.define("config:get-app-config", () => config);
|
|
31
31
|
handlers.define("cwd", () => process.cwd());
|
|
32
|
-
// Empty defaults so
|
|
33
|
-
//
|
|
32
|
+
// Empty defaults so advisors can wrap these regardless of load order;
|
|
33
|
+
// system-prompt:frontend is where the active frontend describes its surface.
|
|
34
34
|
handlers.define("dynamic-context:build", () => "");
|
|
35
35
|
handlers.define("query-context:build", () => "");
|
|
36
|
+
handlers.define("system-prompt:frontend", () => "");
|
|
36
37
|
const backends = new Map();
|
|
37
38
|
let activeBackendName = null;
|
|
38
39
|
bus.on("agent:register-backend", (backend) => {
|
|
@@ -41,16 +41,21 @@ export default function activate(ctx) {
|
|
|
41
41
|
description: "Cycle to next model, or switch to a specific one",
|
|
42
42
|
handler: (args) => {
|
|
43
43
|
const name = args.trim();
|
|
44
|
+
const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
|
|
44
45
|
if (!name) {
|
|
45
|
-
const
|
|
46
|
-
const label = active
|
|
47
|
-
? `${active.model}${active.provider ? ` [${active.provider}]` : ""}`
|
|
48
|
-
: "none";
|
|
46
|
+
const label = active ? `${active.id} [${active.provider}]` : "none";
|
|
49
47
|
bus.emit("ui:info", { message: `Model: ${label}` });
|
|
48
|
+
return;
|
|
50
49
|
}
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const atIdx = name.lastIndexOf("@");
|
|
51
|
+
const id = atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
52
|
+
const providerHint = atIdx > 0 ? name.slice(atIdx + 1) : undefined;
|
|
53
|
+
const found = models.find((m) => m.id === id && (!providerHint || m.provider === providerHint));
|
|
54
|
+
if (!found) {
|
|
55
|
+
bus.emit("ui:error", { message: `Unknown model: ${name}` });
|
|
56
|
+
return;
|
|
53
57
|
}
|
|
58
|
+
bus.emit("config:switch-model", { id: found.id, provider: found.provider });
|
|
54
59
|
},
|
|
55
60
|
});
|
|
56
61
|
register({
|
|
@@ -163,16 +168,16 @@ export default function activate(ctx) {
|
|
|
163
168
|
const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
|
|
164
169
|
const counts = new Map();
|
|
165
170
|
for (const m of models)
|
|
166
|
-
counts.set(m.
|
|
171
|
+
counts.set(m.id, (counts.get(m.id) ?? 0) + 1);
|
|
167
172
|
const items = models
|
|
168
|
-
.filter((m) => m.
|
|
173
|
+
.filter((m) => m.id.toLowerCase().includes(partial))
|
|
169
174
|
.slice(0, 15)
|
|
170
175
|
.map((m) => {
|
|
171
|
-
const ambiguous = (counts.get(m.
|
|
172
|
-
const qualified = ambiguous ? `${m.
|
|
176
|
+
const ambiguous = (counts.get(m.id) ?? 0) > 1;
|
|
177
|
+
const qualified = ambiguous ? `${m.id}@${m.provider}` : m.id;
|
|
173
178
|
return {
|
|
174
179
|
name: `/model ${qualified}`,
|
|
175
|
-
description:
|
|
180
|
+
description: `[${m.provider}]${active && m.id === active.id && m.provider === active.provider ? " (active)" : ""}`,
|
|
176
181
|
};
|
|
177
182
|
});
|
|
178
183
|
if (items.length === 0)
|
package/dist/shell/index.js
CHANGED
|
@@ -7,11 +7,13 @@ import "./events.js"; // augments BusEvents with shell-owned events
|
|
|
7
7
|
import { Shell } from "./shell.js";
|
|
8
8
|
import { DefaultCompositor } from "../utils/compositor.js";
|
|
9
9
|
import { TerminalBuffer } from "../utils/terminal-buffer.js";
|
|
10
|
+
import { FloatingPanel } from "../utils/floating-panel.js";
|
|
10
11
|
import { setPalette } from "../utils/palette.js";
|
|
11
12
|
import * as streamTransform from "../utils/stream-transform.js";
|
|
12
13
|
import activateShellContext from "./shell-context.js";
|
|
13
14
|
import activateTuiRenderer from "./tui-renderer.js";
|
|
14
15
|
import { processTerminal, surfaceFromTerminal } from "./terminal.js";
|
|
16
|
+
const SHELL_SURFACE = `You're attached through a terminal shell. It shares the user's working directory, environment, and command history, and you can act on their live session — everything they run at the prompt is visible to you.`;
|
|
15
17
|
/**
|
|
16
18
|
* Register shell-owned handlers extensions can `ctx.call`, and attach
|
|
17
19
|
* the shell surface to ctx. Must run before `loadExtensions` so user
|
|
@@ -20,6 +22,10 @@ import { processTerminal, surfaceFromTerminal } from "./terminal.js";
|
|
|
20
22
|
export function registerShellHandlers(ctx) {
|
|
21
23
|
const { bus } = ctx;
|
|
22
24
|
const compositor = new DefaultCompositor(bus);
|
|
25
|
+
ctx.advise("system-prompt:frontend", (next) => {
|
|
26
|
+
const base = next() ?? "";
|
|
27
|
+
return base ? `${base}\n\n${SHELL_SURFACE}` : SHELL_SURFACE;
|
|
28
|
+
});
|
|
23
29
|
const shellSurface = {
|
|
24
30
|
compositor,
|
|
25
31
|
setPalette,
|
|
@@ -69,6 +75,9 @@ export function registerShellHandlers(ctx) {
|
|
|
69
75
|
terminalBufferSingleton = TerminalBuffer.createWired(ctx.bus);
|
|
70
76
|
return terminalBufferSingleton;
|
|
71
77
|
});
|
|
78
|
+
// bus override lets callers pass their scoped bus, so the panel's
|
|
79
|
+
// listeners unwire when the extension reloads.
|
|
80
|
+
ctx.define("floating-panel:create", (config, bus) => new FloatingPanel(bus ?? ctx.bus, config));
|
|
72
81
|
activateShellContext(ctx);
|
|
73
82
|
activateTuiRenderer(ctx);
|
|
74
83
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Tracks PTY commands and cwd, spills long outputs, contributes per-query
|
|
2
|
-
* `<
|
|
3
|
-
* without a PTY skip this
|
|
2
|
+
* `<shell_events>` (fresh user exchanges) and — under the shell frontend —
|
|
3
|
+
* `<cwd>`. Frontends without a PTY skip this. */
|
|
4
4
|
import type { ExtensionContext } from "./host-types.js";
|
|
5
5
|
export default function activate(ctx: ExtensionContext): void;
|