hoomanjs 1.0.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/.github/screenshot.png +0 -0
- package/.github/workflows/build-publish.yml +49 -0
- package/LICENSE +21 -0
- package/README.md +399 -0
- package/docker-compose.yml +13 -0
- package/package.json +78 -0
- package/src/acp/acp-agent.ts +803 -0
- package/src/acp/approvals.ts +147 -0
- package/src/acp/index.ts +1 -0
- package/src/acp/meta/system-prompt.ts +44 -0
- package/src/acp/meta/user-id.ts +44 -0
- package/src/acp/prompt-invoke.ts +149 -0
- package/src/acp/sessions/config-options.ts +56 -0
- package/src/acp/sessions/replay.ts +131 -0
- package/src/acp/sessions/store.ts +158 -0
- package/src/acp/sessions/title.ts +22 -0
- package/src/acp/utils/paths.ts +5 -0
- package/src/acp/utils/tool-kind.ts +38 -0
- package/src/acp/utils/tool-locations.ts +46 -0
- package/src/acp/utils/tool-result-content.ts +27 -0
- package/src/chat/app.tsx +428 -0
- package/src/chat/approvals.ts +96 -0
- package/src/chat/components/ApprovalPrompt.tsx +25 -0
- package/src/chat/components/ChatMessage.tsx +47 -0
- package/src/chat/components/Composer.tsx +39 -0
- package/src/chat/components/EmptyChatBanner.tsx +26 -0
- package/src/chat/components/ReasoningStrip.tsx +30 -0
- package/src/chat/components/Spinner.tsx +34 -0
- package/src/chat/components/StatusBar.tsx +65 -0
- package/src/chat/components/ThinkingStatus.tsx +128 -0
- package/src/chat/components/ToolEvent.tsx +34 -0
- package/src/chat/components/Transcript.tsx +34 -0
- package/src/chat/components/ascii-logo.ts +11 -0
- package/src/chat/components/shared.ts +70 -0
- package/src/chat/index.tsx +42 -0
- package/src/chat/types.ts +21 -0
- package/src/cli.ts +146 -0
- package/src/configure/app.tsx +911 -0
- package/src/configure/components/BusyScreen.tsx +22 -0
- package/src/configure/components/HomeScreen.tsx +43 -0
- package/src/configure/components/MenuScreen.tsx +44 -0
- package/src/configure/components/PromptForm.tsx +40 -0
- package/src/configure/components/SelectMenuItem.tsx +30 -0
- package/src/configure/index.tsx +43 -0
- package/src/configure/open-in-editor.ts +133 -0
- package/src/configure/types.ts +45 -0
- package/src/configure/utils.ts +113 -0
- package/src/core/agent/index.ts +76 -0
- package/src/core/config.ts +157 -0
- package/src/core/index.ts +54 -0
- package/src/core/mcp/config.ts +80 -0
- package/src/core/mcp/index.ts +13 -0
- package/src/core/mcp/manager.ts +109 -0
- package/src/core/mcp/prefixed-mcp-tool.ts +45 -0
- package/src/core/mcp/tools.ts +92 -0
- package/src/core/mcp/types.ts +37 -0
- package/src/core/memory/index.ts +17 -0
- package/src/core/memory/ltm/embed.ts +67 -0
- package/src/core/memory/ltm/index.ts +18 -0
- package/src/core/memory/ltm/store.ts +376 -0
- package/src/core/memory/ltm/tools.ts +146 -0
- package/src/core/memory/ltm/types.ts +111 -0
- package/src/core/memory/ltm/utils.ts +218 -0
- package/src/core/memory/stm/index.ts +17 -0
- package/src/core/models/anthropic.ts +53 -0
- package/src/core/models/bedrock.ts +54 -0
- package/src/core/models/google.ts +51 -0
- package/src/core/models/index.ts +16 -0
- package/src/core/models/ollama/index.ts +13 -0
- package/src/core/models/ollama/strands-ollama.ts +439 -0
- package/src/core/models/openai.ts +12 -0
- package/src/core/prompts/index.ts +23 -0
- package/src/core/prompts/skills.ts +66 -0
- package/src/core/prompts/static/fetch.md +33 -0
- package/src/core/prompts/static/filesystem.md +38 -0
- package/src/core/prompts/static/identity.md +22 -0
- package/src/core/prompts/static/ltm.md +39 -0
- package/src/core/prompts/static/memory.md +39 -0
- package/src/core/prompts/static/shell.md +34 -0
- package/src/core/prompts/static/skills.md +19 -0
- package/src/core/prompts/static/thinking.md +27 -0
- package/src/core/prompts/system.ts +109 -0
- package/src/core/skills/index.ts +2 -0
- package/src/core/skills/registry.ts +239 -0
- package/src/core/skills/tools.ts +80 -0
- package/src/core/toolkit.ts +13 -0
- package/src/core/tools/fetch.ts +288 -0
- package/src/core/tools/filesystem.ts +747 -0
- package/src/core/tools/index.ts +5 -0
- package/src/core/tools/shell.ts +426 -0
- package/src/core/tools/thinking.ts +184 -0
- package/src/core/tools/time.ts +121 -0
- package/src/core/utils/cwd-context.ts +11 -0
- package/src/core/utils/paths.ts +28 -0
- package/src/exec/approvals.ts +85 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContextWindowOverflowError,
|
|
3
|
+
Model,
|
|
4
|
+
ModelError,
|
|
5
|
+
} from "@strands-agents/sdk";
|
|
6
|
+
import type { SystemPrompt } from "@strands-agents/sdk";
|
|
7
|
+
import type { BaseModelConfig, StreamOptions } from "@strands-agents/sdk";
|
|
8
|
+
import {
|
|
9
|
+
ModelContentBlockDeltaEvent,
|
|
10
|
+
ModelContentBlockStartEvent,
|
|
11
|
+
ModelContentBlockStopEvent,
|
|
12
|
+
ModelMessageStartEvent,
|
|
13
|
+
ModelMessageStopEvent,
|
|
14
|
+
ModelMetadataEvent,
|
|
15
|
+
} from "@strands-agents/sdk";
|
|
16
|
+
import type { ModelStreamEvent } from "@strands-agents/sdk";
|
|
17
|
+
import type { ToolSpec } from "@strands-agents/sdk";
|
|
18
|
+
import { Message, ToolResultBlock, ToolUseBlock } from "@strands-agents/sdk";
|
|
19
|
+
import type {
|
|
20
|
+
ContentBlock,
|
|
21
|
+
ImageBlock,
|
|
22
|
+
JsonBlock,
|
|
23
|
+
ToolResultContent,
|
|
24
|
+
} from "@strands-agents/sdk";
|
|
25
|
+
import { Ollama } from "ollama";
|
|
26
|
+
import type {
|
|
27
|
+
ChatRequest,
|
|
28
|
+
ChatResponse,
|
|
29
|
+
Message as OllamaMessage,
|
|
30
|
+
Tool as OllamaTool,
|
|
31
|
+
} from "ollama";
|
|
32
|
+
|
|
33
|
+
export interface OllamaModelConfig extends BaseModelConfig {
|
|
34
|
+
modelId?: string;
|
|
35
|
+
/** Ollama server URL (default `http://127.0.0.1:11434` or `OLLAMA_HOST`). */
|
|
36
|
+
host?: string;
|
|
37
|
+
/** Passed through to Ollama `keep_alive`. */
|
|
38
|
+
keepAlive?: string | number;
|
|
39
|
+
/** Merged into Ollama `options` (e.g. num_ctx). */
|
|
40
|
+
options?: Record<string, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Ollama `think` flag (controls `message.thinking` on supported models).
|
|
43
|
+
* - **Omitted (`undefined`):** do not send `think` — server default (often streams `thinking`, which we map to
|
|
44
|
+
* Strands `ReasoningBlock` via `reasoningContentDelta`).
|
|
45
|
+
* - **`false`:** disable the thinking channel; the model puts prose in `content` only (shows up as normal
|
|
46
|
+
* `TextBlock` in persisted sessions).
|
|
47
|
+
* - **`true` or `"high" | "medium" | "low"`:** force extended thinking at that level.
|
|
48
|
+
*/
|
|
49
|
+
think?: boolean | "high" | "medium" | "low";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractSystemText(system?: SystemPrompt): string | undefined {
|
|
53
|
+
if (system === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (typeof system === "string") {
|
|
57
|
+
return system;
|
|
58
|
+
}
|
|
59
|
+
const parts: string[] = [];
|
|
60
|
+
for (const block of system) {
|
|
61
|
+
if (block.type === "textBlock") {
|
|
62
|
+
parts.push(block.text);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const joined = parts.join("\n").trim();
|
|
66
|
+
return joined.length > 0 ? joined : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One Ollama chat message carrying an image (bytes or URL string). */
|
|
70
|
+
function formatImageBlock(
|
|
71
|
+
role: "user" | "assistant" | "tool",
|
|
72
|
+
block: ImageBlock,
|
|
73
|
+
): OllamaMessage[] {
|
|
74
|
+
const src = block.source;
|
|
75
|
+
if (src.type === "imageSourceBytes") {
|
|
76
|
+
return [{ role, content: "", images: [src.bytes] }];
|
|
77
|
+
}
|
|
78
|
+
if (src.type === "imageSourceUrl") {
|
|
79
|
+
return [{ role, content: "", images: [src.url] }];
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
role,
|
|
84
|
+
content:
|
|
85
|
+
"(Ollama: image sources other than bytes or URL are not supported for this provider)",
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Flatten a tool result into one Ollama `role: "tool"` message per content item, matching
|
|
92
|
+
* Python `OllamaModel._format_request_message_contents` for `toolResult` (text, JSON as text,
|
|
93
|
+
* nested images as `images`, etc.).
|
|
94
|
+
*/
|
|
95
|
+
function formatToolResultContentsToOllama(
|
|
96
|
+
block: ToolResultBlock,
|
|
97
|
+
): OllamaMessage[] {
|
|
98
|
+
const out: OllamaMessage[] = [];
|
|
99
|
+
for (const c of block.content) {
|
|
100
|
+
out.push(...formatToolResultContentToOllama(c));
|
|
101
|
+
}
|
|
102
|
+
if (out.length > 0) {
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
const fallback = block.status === "error" ? "(tool error)" : "";
|
|
106
|
+
return [{ role: "tool", content: fallback }];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatToolResultContentToOllama(
|
|
110
|
+
c: ToolResultContent,
|
|
111
|
+
): OllamaMessage[] {
|
|
112
|
+
if (c.type === "textBlock") {
|
|
113
|
+
return [{ role: "tool", content: c.text }];
|
|
114
|
+
}
|
|
115
|
+
if (c.type === "jsonBlock") {
|
|
116
|
+
const j = c as JsonBlock;
|
|
117
|
+
return [{ role: "tool", content: JSON.stringify(j.json) }];
|
|
118
|
+
}
|
|
119
|
+
if (c.type === "imageBlock") {
|
|
120
|
+
return formatImageBlock("tool", c as ImageBlock);
|
|
121
|
+
}
|
|
122
|
+
if (c.type === "videoBlock" || c.type === "documentBlock") {
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
role: "tool",
|
|
126
|
+
content: `(Ollama: tool result ${c.type} is not supported)`,
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatContentBlock(
|
|
134
|
+
role: "user" | "assistant",
|
|
135
|
+
block: ContentBlock,
|
|
136
|
+
): OllamaMessage[] {
|
|
137
|
+
if (block.type === "textBlock") {
|
|
138
|
+
return [{ role, content: block.text }];
|
|
139
|
+
}
|
|
140
|
+
if (block.type === "imageBlock") {
|
|
141
|
+
return formatImageBlock(role, block as ImageBlock);
|
|
142
|
+
}
|
|
143
|
+
if (block.type === "toolUseBlock") {
|
|
144
|
+
const b = block as ToolUseBlock;
|
|
145
|
+
const args =
|
|
146
|
+
typeof b.input === "object" && b.input !== null && !Array.isArray(b.input)
|
|
147
|
+
? (b.input as Record<string, unknown>)
|
|
148
|
+
: {};
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
role: "assistant",
|
|
152
|
+
content: "",
|
|
153
|
+
tool_calls: [
|
|
154
|
+
{
|
|
155
|
+
function: {
|
|
156
|
+
name: b.name,
|
|
157
|
+
arguments: args,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
if (block.type === "toolResultBlock") {
|
|
165
|
+
return formatToolResultContentsToOllama(block as ToolResultBlock);
|
|
166
|
+
}
|
|
167
|
+
if (block.type === "reasoningBlock") {
|
|
168
|
+
// Ollama chat history has no structured reasoning part. Re-sending prior turns' chain
|
|
169
|
+
// of thought as plain text would pollute `content` and break separation in persistence — omit.
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function strandsMessagesToOllama(
|
|
176
|
+
messages: Message[],
|
|
177
|
+
systemText: string | undefined,
|
|
178
|
+
): OllamaMessage[] {
|
|
179
|
+
const out: OllamaMessage[] = [];
|
|
180
|
+
if (systemText) {
|
|
181
|
+
out.push({ role: "system", content: systemText });
|
|
182
|
+
}
|
|
183
|
+
for (const msg of messages) {
|
|
184
|
+
for (const block of msg.content) {
|
|
185
|
+
out.push(...formatContentBlock(msg.role, block));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function strandsToolsToOllama(
|
|
192
|
+
toolSpecs: ToolSpec[] | undefined,
|
|
193
|
+
): OllamaTool[] | undefined {
|
|
194
|
+
if (!toolSpecs?.length) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
return toolSpecs.map(
|
|
198
|
+
(spec) =>
|
|
199
|
+
({
|
|
200
|
+
type: "function",
|
|
201
|
+
function: {
|
|
202
|
+
name: spec.name,
|
|
203
|
+
description: spec.description,
|
|
204
|
+
parameters: spec.inputSchema ?? { type: "object", properties: {} },
|
|
205
|
+
},
|
|
206
|
+
}) as OllamaTool,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function mapDoneReason(reason: string | undefined): "endTurn" | "maxTokens" {
|
|
211
|
+
const r = (reason ?? "").toLowerCase();
|
|
212
|
+
if (r.includes("length") || r === "max_tokens") {
|
|
213
|
+
return "maxTokens";
|
|
214
|
+
}
|
|
215
|
+
return "endTurn";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Stable key so we do not re-emit identical `tool_calls` on repeated stream chunks. */
|
|
219
|
+
function toolCallsSnapshotKey(
|
|
220
|
+
calls: NonNullable<NonNullable<ChatResponse["message"]>["tool_calls"]>,
|
|
221
|
+
): string {
|
|
222
|
+
return JSON.stringify(
|
|
223
|
+
calls.map((tc) => ({
|
|
224
|
+
name: tc.function?.name,
|
|
225
|
+
args: tc.function?.arguments,
|
|
226
|
+
})),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export class StrandsOllamaModel extends Model<OllamaModelConfig> {
|
|
231
|
+
private config: OllamaModelConfig;
|
|
232
|
+
private readonly client: Ollama;
|
|
233
|
+
|
|
234
|
+
constructor(config: OllamaModelConfig) {
|
|
235
|
+
super();
|
|
236
|
+
this.config = { ...config };
|
|
237
|
+
this.client = new Ollama({
|
|
238
|
+
host:
|
|
239
|
+
this.config.host ?? process.env.OLLAMA_HOST ?? "http://127.0.0.1:11434",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
updateConfig(modelConfig: OllamaModelConfig): void {
|
|
244
|
+
this.config = { ...this.config, ...modelConfig };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getConfig(): OllamaModelConfig {
|
|
248
|
+
return { ...this.config };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async *stream(
|
|
252
|
+
messages: Message[],
|
|
253
|
+
options?: StreamOptions,
|
|
254
|
+
): AsyncIterable<ModelStreamEvent> {
|
|
255
|
+
const modelId = this.config.modelId;
|
|
256
|
+
if (!modelId) {
|
|
257
|
+
throw new ModelError("Ollama modelId is not configured");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const systemText = extractSystemText(options?.systemPrompt);
|
|
261
|
+
const ollamaMessages = strandsMessagesToOllama(messages, systemText);
|
|
262
|
+
const tools = strandsToolsToOllama(options?.toolSpecs);
|
|
263
|
+
|
|
264
|
+
const request: ChatRequest = {
|
|
265
|
+
model: modelId,
|
|
266
|
+
messages: ollamaMessages,
|
|
267
|
+
stream: true,
|
|
268
|
+
tools,
|
|
269
|
+
options: {
|
|
270
|
+
num_predict: this.config.maxTokens,
|
|
271
|
+
temperature: this.config.temperature,
|
|
272
|
+
top_p: this.config.topP,
|
|
273
|
+
...(this.config.options ?? {}),
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
if (this.config.think !== undefined) {
|
|
277
|
+
request.think = this.config.think;
|
|
278
|
+
}
|
|
279
|
+
if (this.config.keepAlive !== undefined) {
|
|
280
|
+
request.keep_alive = this.config.keepAlive;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let stream: AsyncIterable<ChatResponse>;
|
|
284
|
+
try {
|
|
285
|
+
stream = await this.client.chat({ ...request, stream: true });
|
|
286
|
+
} catch (e) {
|
|
287
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
288
|
+
if (msg.toLowerCase().includes("context") || msg.includes("token")) {
|
|
289
|
+
throw new ContextWindowOverflowError(msg);
|
|
290
|
+
}
|
|
291
|
+
throw new ModelError(`Ollama chat error: ${msg}`, { cause: e });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// `message.content` is treated as an incremental fragment per chunk (Ollama / ollama-js).
|
|
295
|
+
// **Tool calls:** many models (e.g. gemma4:e4b) send `tool_calls` only on `done: false` chunks;
|
|
296
|
+
// the final `done: true` line often omits `tool_calls`, so we must not require `part.done`.
|
|
297
|
+
// Emit tools when a new snapshot differs from the last emitted (Strands concatenates tool deltas).
|
|
298
|
+
yield new ModelMessageStartEvent({
|
|
299
|
+
type: "modelMessageStartEvent",
|
|
300
|
+
role: "assistant",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
let last: ChatResponse | undefined;
|
|
304
|
+
let toolRequested = false;
|
|
305
|
+
let textBlockOpen = false;
|
|
306
|
+
let reasoningBlockOpen = false;
|
|
307
|
+
let lastEmittedToolCallsKey: string | undefined;
|
|
308
|
+
|
|
309
|
+
for await (const part of stream) {
|
|
310
|
+
last = part;
|
|
311
|
+
const msg = part.message;
|
|
312
|
+
if (!msg) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const calls = msg.tool_calls;
|
|
317
|
+
if (calls?.length) {
|
|
318
|
+
const snapKey = toolCallsSnapshotKey(calls);
|
|
319
|
+
if (snapKey !== lastEmittedToolCallsKey) {
|
|
320
|
+
lastEmittedToolCallsKey = snapKey;
|
|
321
|
+
toolRequested = true;
|
|
322
|
+
if (reasoningBlockOpen) {
|
|
323
|
+
yield new ModelContentBlockStopEvent({
|
|
324
|
+
type: "modelContentBlockStopEvent",
|
|
325
|
+
});
|
|
326
|
+
reasoningBlockOpen = false;
|
|
327
|
+
}
|
|
328
|
+
if (textBlockOpen) {
|
|
329
|
+
yield new ModelContentBlockStopEvent({
|
|
330
|
+
type: "modelContentBlockStopEvent",
|
|
331
|
+
});
|
|
332
|
+
textBlockOpen = false;
|
|
333
|
+
}
|
|
334
|
+
let toolIndex = 0;
|
|
335
|
+
for (const toolCall of calls) {
|
|
336
|
+
const name = toolCall.function?.name ?? "tool";
|
|
337
|
+
const apiId = (toolCall as { id?: string }).id;
|
|
338
|
+
const toolUseId =
|
|
339
|
+
apiId ?? (calls.length > 1 ? `${name}_${toolIndex}` : name);
|
|
340
|
+
toolIndex += 1;
|
|
341
|
+
const argsRaw = toolCall.function?.arguments;
|
|
342
|
+
const inputStr =
|
|
343
|
+
typeof argsRaw === "string"
|
|
344
|
+
? argsRaw
|
|
345
|
+
: JSON.stringify(argsRaw ?? {});
|
|
346
|
+
yield new ModelContentBlockStartEvent({
|
|
347
|
+
type: "modelContentBlockStartEvent",
|
|
348
|
+
start: { type: "toolUseStart", name, toolUseId },
|
|
349
|
+
});
|
|
350
|
+
yield new ModelContentBlockDeltaEvent({
|
|
351
|
+
type: "modelContentBlockDeltaEvent",
|
|
352
|
+
delta: {
|
|
353
|
+
type: "toolUseInputDelta",
|
|
354
|
+
input: inputStr,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
yield new ModelContentBlockStopEvent({
|
|
358
|
+
type: "modelContentBlockStopEvent",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const thinking = (msg as { thinking?: string }).thinking ?? "";
|
|
365
|
+
const content = msg.content ?? "";
|
|
366
|
+
|
|
367
|
+
if (thinking.length > 0) {
|
|
368
|
+
if (textBlockOpen) {
|
|
369
|
+
yield new ModelContentBlockStopEvent({
|
|
370
|
+
type: "modelContentBlockStopEvent",
|
|
371
|
+
});
|
|
372
|
+
textBlockOpen = false;
|
|
373
|
+
}
|
|
374
|
+
if (!reasoningBlockOpen) {
|
|
375
|
+
yield new ModelContentBlockStartEvent({
|
|
376
|
+
type: "modelContentBlockStartEvent",
|
|
377
|
+
});
|
|
378
|
+
reasoningBlockOpen = true;
|
|
379
|
+
}
|
|
380
|
+
yield new ModelContentBlockDeltaEvent({
|
|
381
|
+
type: "modelContentBlockDeltaEvent",
|
|
382
|
+
delta: { type: "reasoningContentDelta", text: thinking },
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (content.length > 0) {
|
|
387
|
+
if (reasoningBlockOpen) {
|
|
388
|
+
yield new ModelContentBlockStopEvent({
|
|
389
|
+
type: "modelContentBlockStopEvent",
|
|
390
|
+
});
|
|
391
|
+
reasoningBlockOpen = false;
|
|
392
|
+
}
|
|
393
|
+
if (!textBlockOpen) {
|
|
394
|
+
yield new ModelContentBlockStartEvent({
|
|
395
|
+
type: "modelContentBlockStartEvent",
|
|
396
|
+
});
|
|
397
|
+
textBlockOpen = true;
|
|
398
|
+
}
|
|
399
|
+
yield new ModelContentBlockDeltaEvent({
|
|
400
|
+
type: "modelContentBlockDeltaEvent",
|
|
401
|
+
delta: { type: "textDelta", text: content },
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (reasoningBlockOpen) {
|
|
407
|
+
yield new ModelContentBlockStopEvent({
|
|
408
|
+
type: "modelContentBlockStopEvent",
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (textBlockOpen) {
|
|
412
|
+
yield new ModelContentBlockStopEvent({
|
|
413
|
+
type: "modelContentBlockStopEvent",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const doneReason = last?.done_reason;
|
|
418
|
+
const stopReason = toolRequested
|
|
419
|
+
? ("toolUse" as const)
|
|
420
|
+
: mapDoneReason(doneReason);
|
|
421
|
+
|
|
422
|
+
yield new ModelMessageStopEvent({
|
|
423
|
+
type: "modelMessageStopEvent",
|
|
424
|
+
stopReason,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
yield new ModelMetadataEvent({
|
|
428
|
+
type: "modelMetadataEvent",
|
|
429
|
+
usage: {
|
|
430
|
+
inputTokens: last?.prompt_eval_count ?? 0,
|
|
431
|
+
outputTokens: last?.eval_count ?? 0,
|
|
432
|
+
totalTokens: (last?.prompt_eval_count ?? 0) + (last?.eval_count ?? 0),
|
|
433
|
+
},
|
|
434
|
+
metrics: last?.total_duration
|
|
435
|
+
? { latencyMs: last.total_duration / 1_000_000 }
|
|
436
|
+
: undefined,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Config } from "../config.ts";
|
|
2
|
+
import type { Registry } from "../skills/registry.ts";
|
|
3
|
+
import type { Toolkit } from "../toolkit.ts";
|
|
4
|
+
import { Skills } from "./skills.ts";
|
|
5
|
+
import { System } from "./system.ts";
|
|
6
|
+
|
|
7
|
+
export { Skills, System };
|
|
8
|
+
|
|
9
|
+
export async function system(
|
|
10
|
+
path: string,
|
|
11
|
+
config: Config,
|
|
12
|
+
toolkit: Toolkit,
|
|
13
|
+
): Promise<System> {
|
|
14
|
+
const prompt = new System(path, config, toolkit);
|
|
15
|
+
await prompt.reload();
|
|
16
|
+
return prompt;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function skills(registry: Registry): Promise<Skills> {
|
|
20
|
+
const prompt = new Skills(registry);
|
|
21
|
+
await prompt.reload();
|
|
22
|
+
return prompt;
|
|
23
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import type { Registry } from "../skills/registry.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds the dynamic **Available skills** markdown block using
|
|
7
|
+
* {@link Registry.list} (same source as the skills CLI).
|
|
8
|
+
*
|
|
9
|
+
* Mirrors {@link System}: call {@link reload}, then read {@link content}.
|
|
10
|
+
*/
|
|
11
|
+
export class Skills {
|
|
12
|
+
private readonly registry: Registry;
|
|
13
|
+
private data = "";
|
|
14
|
+
|
|
15
|
+
public constructor(registry: Registry) {
|
|
16
|
+
this.registry = registry;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get content(): string {
|
|
20
|
+
return this.data;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public async reload(): Promise<void> {
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = await this.registry.list();
|
|
27
|
+
} catch {
|
|
28
|
+
this.data = [
|
|
29
|
+
"## Available skills",
|
|
30
|
+
"",
|
|
31
|
+
"The skills list could not be loaded (for example if `npx skills` is unavailable). Skills management tools may still work; try again later.",
|
|
32
|
+
"",
|
|
33
|
+
].join("\n");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lines: string[] = [
|
|
38
|
+
"## Available skills",
|
|
39
|
+
"",
|
|
40
|
+
"Installed skills for this environment. When one matches the user's request or would materially help the current turn, read its `SKILL.md` at the path shown (for example with your file-reading tool) and follow that guidance.",
|
|
41
|
+
"",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
if (entries.length === 0) {
|
|
45
|
+
lines.push(
|
|
46
|
+
"No skills are installed yet. Use the skills management tools to search and install packages, or add them under your agent skills directory.",
|
|
47
|
+
"",
|
|
48
|
+
);
|
|
49
|
+
this.data = lines.join("\n").trim();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sorted = [...entries].sort((a, b) =>
|
|
54
|
+
a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
for (const e of sorted) {
|
|
58
|
+
lines.push(`- **${e.name}**`);
|
|
59
|
+
lines.push(` - Description: ${e.description}`);
|
|
60
|
+
lines.push(` - Path: \`${e.path}\``);
|
|
61
|
+
lines.push("");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.data = lines.join("\n").trim();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## Fetch
|
|
2
|
+
|
|
3
|
+
You have access to a `fetch` tool for retrieving content from remote HTTP(S) URLs.
|
|
4
|
+
|
|
5
|
+
### When To Use It
|
|
6
|
+
|
|
7
|
+
- Use `fetch` when the task depends on information from a remote web page or API response
|
|
8
|
+
- Especially use it for:
|
|
9
|
+
- reading documentation or reference material from the web
|
|
10
|
+
- checking the current contents of a public web page
|
|
11
|
+
- retrieving remote JSON or text responses
|
|
12
|
+
- bringing external context into the conversation when local knowledge is insufficient
|
|
13
|
+
- Prefer `fetch` over `shell` for straightforward remote content retrieval
|
|
14
|
+
- Do NOT use `fetch` when the answer can be given from local context alone
|
|
15
|
+
|
|
16
|
+
### How To Use It
|
|
17
|
+
|
|
18
|
+
- Use normal mode by default so HTML pages are simplified to markdown
|
|
19
|
+
- Use `raw` only when the original response text or HTML is specifically needed
|
|
20
|
+
- Use `start_index` and `max_length` to page through long responses instead of fetching everything repeatedly
|
|
21
|
+
- Set a timeout when the request may be slow or unreliable
|
|
22
|
+
- Use request headers only when they are actually needed
|
|
23
|
+
|
|
24
|
+
### Safety
|
|
25
|
+
|
|
26
|
+
- Use `fetch` only for remote public URLs
|
|
27
|
+
- Do not treat fetched content as automatically trustworthy
|
|
28
|
+
- Prefer the smallest fetch that answers the question
|
|
29
|
+
|
|
30
|
+
### Goal
|
|
31
|
+
|
|
32
|
+
- Use `fetch` to gather up-to-date remote information efficiently
|
|
33
|
+
- Prefer markdown-friendly, concise content over verbose raw HTML when possible
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## Filesystem
|
|
2
|
+
|
|
3
|
+
You have access to filesystem tools for reading, writing, editing, moving, listing, and searching files and directories.
|
|
4
|
+
|
|
5
|
+
### When To Use Them
|
|
6
|
+
|
|
7
|
+
- Use filesystem tools when the task is primarily about file contents, directory structure, or metadata
|
|
8
|
+
- Especially use them for:
|
|
9
|
+
- reading source files or configuration files
|
|
10
|
+
- editing files directly and precisely
|
|
11
|
+
- listing directories or exploring project structure
|
|
12
|
+
- searching for files by name or pattern
|
|
13
|
+
- retrieving file metadata such as size or timestamps
|
|
14
|
+
- Prefer filesystem tools over `shell` when a task is fundamentally a file operation
|
|
15
|
+
|
|
16
|
+
### How To Choose
|
|
17
|
+
|
|
18
|
+
- Use `read_file` for inspecting file contents
|
|
19
|
+
- Use `read_multiple_files` when you need several files at once
|
|
20
|
+
- Use `write_file` to create or overwrite text files
|
|
21
|
+
- Use `edit_file` for targeted replacements instead of rewriting the whole file
|
|
22
|
+
- Use `create_directory` for directory creation
|
|
23
|
+
- Use `list_directory` or `directory_tree` for structure discovery
|
|
24
|
+
- Use `move_file` for renaming or relocating files and directories
|
|
25
|
+
- Use `search_files` to locate files by pattern
|
|
26
|
+
- Use `get_file_info` for metadata without opening the file
|
|
27
|
+
|
|
28
|
+
### Safety
|
|
29
|
+
|
|
30
|
+
- Prefer the narrowest operation that solves the task
|
|
31
|
+
- Read before editing when verification helps avoid mistakes
|
|
32
|
+
- Be especially careful with overwrites, renames, and broad recursive operations
|
|
33
|
+
- Avoid unnecessary file churn when a smaller edit is sufficient
|
|
34
|
+
|
|
35
|
+
### Goal
|
|
36
|
+
|
|
37
|
+
- Use filesystem tools for direct, precise file work
|
|
38
|
+
- Keep file operations intentional, minimal, and easy to verify
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## Identity
|
|
2
|
+
|
|
3
|
+
You are an agent named **{{ name }}**.
|
|
4
|
+
You are a reliable, structured, and thoughtful assistant.
|
|
5
|
+
|
|
6
|
+
### Behavior
|
|
7
|
+
|
|
8
|
+
- Be clear, concise, and practical
|
|
9
|
+
- Prefer structured answers when helpful
|
|
10
|
+
- Avoid unnecessary verbosity
|
|
11
|
+
|
|
12
|
+
### Decision Making
|
|
13
|
+
|
|
14
|
+
- Use available tools when they improve accuracy or context
|
|
15
|
+
- Do not rely on tools unnecessarily
|
|
16
|
+
- Prioritize correctness over speed
|
|
17
|
+
|
|
18
|
+
### Interaction Style
|
|
19
|
+
|
|
20
|
+
- Be direct and helpful
|
|
21
|
+
- Do not make assumptions without evidence
|
|
22
|
+
- Ask for clarification if needed
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
## Memory
|
|
2
|
+
|
|
3
|
+
You have access to long-term memory tools.
|
|
4
|
+
|
|
5
|
+
### Retrieval (search_memory)
|
|
6
|
+
|
|
7
|
+
- Use memory when the current request may depend on past interactions, preferences, or ongoing tasks
|
|
8
|
+
- Especially use it for:
|
|
9
|
+
- follow-ups ("last time", "previously", "continue")
|
|
10
|
+
- user-specific preferences or history
|
|
11
|
+
- long-running tasks or projects
|
|
12
|
+
- Do NOT search memory for simple, self-contained questions
|
|
13
|
+
|
|
14
|
+
### Storage (store_memory)
|
|
15
|
+
|
|
16
|
+
- Only store information that is:
|
|
17
|
+
- reusable across conversations
|
|
18
|
+
- specific to the user (preferences, facts, goals, tasks)
|
|
19
|
+
- Good examples:
|
|
20
|
+
- "User prefers TypeScript"
|
|
21
|
+
- "User is building a CV SaaS"
|
|
22
|
+
- Do NOT store:
|
|
23
|
+
- one-off questions
|
|
24
|
+
- temporary context
|
|
25
|
+
- obvious or generic information
|
|
26
|
+
|
|
27
|
+
### Updates (update_memory)
|
|
28
|
+
|
|
29
|
+
- If new information corrects or refines an existing memory, update it instead of creating a new one
|
|
30
|
+
|
|
31
|
+
### Archival (archive_memory)
|
|
32
|
+
|
|
33
|
+
- If a memory becomes irrelevant, outdated, or incorrect, archive it instead of deleting
|
|
34
|
+
|
|
35
|
+
### General Rules
|
|
36
|
+
|
|
37
|
+
- Avoid redundant or duplicate memory
|
|
38
|
+
- Keep memory concise and compressed
|
|
39
|
+
- Prioritize current context over memory if they conflict
|