@tyvm/knowhow 0.0.137 → 0.0.140
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/autodoc/chat-guide.md +1 -0
- package/package.json +5 -2
- package/scripts/build-for-node.sh +4 -4
- package/src/agents/base/base.ts +254 -29
- package/src/agents/patcher/patcher.ts +1 -1
- package/src/agents/researcher/researcher.ts +2 -2
- package/src/agents/tools/agentCall.ts +6 -0
- package/src/agents/tools/connectAgent.ts +353 -0
- package/src/agents/tools/index.ts +6 -0
- package/src/agents/tools/list.ts +48 -0
- package/src/agents/tools/observe.ts +245 -0
- package/src/agents/tools/replyToParent.ts +122 -0
- package/src/agents/tools/runGenerate.ts +209 -0
- package/src/agents/tools/sendAgentMessage.ts +138 -0
- package/src/agents/tools/startAgentTask.ts +92 -5
- package/src/agents/tools/waitForAgentCompleted.ts +269 -0
- package/src/ai.ts +1 -0
- package/src/chat/modules/AgentModule.ts +89 -27
- package/src/chat/modules/SessionsModule.ts +6 -10
- package/src/clients/contextLimits.ts +26 -0
- package/src/clients/pricing/anthropic.ts +12 -0
- package/src/commands/agent.ts +22 -0
- package/src/commands/agents.ts +83 -0
- package/src/commands/hash.ts +3 -2
- package/src/commands/misc.ts +6 -0
- package/src/commands/services.ts +14 -0
- package/src/generate.ts +25 -3
- package/src/services/AgentService.ts +21 -3
- package/src/services/AgentSyncFs.ts +56 -0
- package/src/services/AgentSyncKnowhowWeb.ts +1 -1
- package/src/services/EventService.ts +4 -1
- package/src/services/KnowhowClient.ts +7 -0
- package/src/services/LazyToolsService.ts +7 -3
- package/src/services/MinimalToolsService.ts +15 -7
- package/src/services/SyncedAgentWatcher.ts +2 -0
- package/src/services/SyncerService.ts +35 -6
- package/src/services/Tools.ts +43 -3
- package/src/services/modules/index.ts +127 -46
- package/src/types.ts +5 -0
- package/tests/agents/interrupt.test.ts +11 -18
- package/tests/agents/observe-base.test.ts +183 -0
- package/tests/agents/tools/connectAgent.test.ts +138 -0
- package/tests/agents/tools/observe.test.ts +179 -0
- package/tests/agents/tools/replyToParent.test.ts +125 -0
- package/tests/agents/tools/sendAgentMessage.test.ts +99 -0
- package/tests/agents/tools/waitForAgentCompleted.test.ts +147 -0
- package/tests/chat/detach-attach-rendering.test.ts +315 -0
- package/tests/clients/contextLimits.test.ts +23 -1
- package/tests/clients/pricing.test.ts +20 -1
- package/tests/services/Tools.test.ts +172 -9
- package/tests/unit/modules/moduleLoading.test.ts +69 -0
- package/ts_build/package.json +5 -2
- package/ts_build/src/agents/base/base.d.ts +24 -1
- package/ts_build/src/agents/base/base.js +111 -16
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/patcher/patcher.js +1 -1
- package/ts_build/src/agents/patcher/patcher.js.map +1 -1
- package/ts_build/src/agents/researcher/researcher.js +2 -2
- package/ts_build/src/agents/researcher/researcher.js.map +1 -1
- package/ts_build/src/agents/tools/agentCall.js.map +1 -1
- package/ts_build/src/agents/tools/connectAgent.d.ts +21 -0
- package/ts_build/src/agents/tools/connectAgent.js +284 -0
- package/ts_build/src/agents/tools/connectAgent.js.map +1 -0
- package/ts_build/src/agents/tools/index.d.ts +6 -0
- package/ts_build/src/agents/tools/index.js +6 -0
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +40 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/observe.d.ts +20 -0
- package/ts_build/src/agents/tools/observe.js +144 -0
- package/ts_build/src/agents/tools/observe.js.map +1 -0
- package/ts_build/src/agents/tools/replyToParent.d.ts +4 -0
- package/ts_build/src/agents/tools/replyToParent.js +118 -0
- package/ts_build/src/agents/tools/replyToParent.js.map +1 -0
- package/ts_build/src/agents/tools/runGenerate.d.ts +19 -0
- package/ts_build/src/agents/tools/runGenerate.js +138 -0
- package/ts_build/src/agents/tools/runGenerate.js.map +1 -0
- package/ts_build/src/agents/tools/sendAgentMessage.d.ts +11 -0
- package/ts_build/src/agents/tools/sendAgentMessage.js +118 -0
- package/ts_build/src/agents/tools/sendAgentMessage.js.map +1 -0
- package/ts_build/src/agents/tools/startAgentTask.d.ts +7 -0
- package/ts_build/src/agents/tools/startAgentTask.js +53 -6
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/agents/tools/waitForAgentCompleted.d.ts +10 -0
- package/ts_build/src/agents/tools/waitForAgentCompleted.js +229 -0
- package/ts_build/src/agents/tools/waitForAgentCompleted.js.map +1 -0
- package/ts_build/src/ai.d.ts +1 -0
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +3 -1
- package/ts_build/src/chat/modules/AgentModule.js +46 -21
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +1 -6
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +21 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/pricing/anthropic.d.ts +2 -0
- package/ts_build/src/clients/pricing/anthropic.js +9 -0
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/commands/agent.js +11 -0
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.js +61 -0
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/hash.js +1 -1
- package/ts_build/src/commands/hash.js.map +1 -1
- package/ts_build/src/commands/misc.js +2 -0
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/services.js +10 -0
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/generate.d.ts +2 -0
- package/ts_build/src/generate.js +11 -3
- package/ts_build/src/generate.js.map +1 -1
- package/ts_build/src/services/AgentService.d.ts +4 -1
- package/ts_build/src/services/AgentService.js +9 -2
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
- package/ts_build/src/services/AgentSyncFs.js +35 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -1
- package/ts_build/src/services/EventService.d.ts +4 -1
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.js +3 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +2 -2
- package/ts_build/src/services/LazyToolsService.js +2 -2
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.d.ts +2 -2
- package/ts_build/src/services/MinimalToolsService.js +6 -6
- package/ts_build/src/services/MinimalToolsService.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.d.ts +1 -0
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/SyncerService.d.ts +2 -1
- package/ts_build/src/services/SyncerService.js +8 -2
- package/ts_build/src/services/SyncerService.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +6 -1
- package/ts_build/src/services/Tools.js +9 -3
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +1 -1
- package/ts_build/src/services/modules/index.js +67 -27
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/types.d.ts +3 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/tests/agents/interrupt.test.d.ts +0 -1
- package/ts_build/tests/agents/interrupt.test.js +3 -13
- package/ts_build/tests/agents/interrupt.test.js.map +1 -1
- package/ts_build/tests/agents/observe-base.test.d.ts +1 -0
- package/ts_build/tests/agents/observe-base.test.js +149 -0
- package/ts_build/tests/agents/observe-base.test.js.map +1 -0
- package/ts_build/tests/agents/tools/connectAgent.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/connectAgent.test.js +142 -0
- package/ts_build/tests/agents/tools/connectAgent.test.js.map +1 -0
- package/ts_build/tests/agents/tools/observe.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/observe.test.js +148 -0
- package/ts_build/tests/agents/tools/observe.test.js.map +1 -0
- package/ts_build/tests/agents/tools/replyToParent.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/replyToParent.test.js +116 -0
- package/ts_build/tests/agents/tools/replyToParent.test.js.map +1 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.js +112 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.js.map +1 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js +147 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js.map +1 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.d.ts +1 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.js +168 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.js.map +1 -0
- package/ts_build/tests/clients/contextLimits.test.js +11 -1
- package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
- package/ts_build/tests/clients/pricing.test.js +10 -1
- package/ts_build/tests/clients/pricing.test.js.map +1 -1
- package/ts_build/tests/services/Tools.test.js +126 -8
- package/ts_build/tests/services/Tools.test.js.map +1 -1
- package/ts_build/tests/unit/modules/moduleLoading.test.js +84 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { Tool } from "../../clients/types";
|
|
4
|
+
import { ToolCallContext } from "../../services/Tools";
|
|
5
|
+
|
|
6
|
+
/** Resolve the agents dir lazily so tests (and cwd changes) are respected. */
|
|
7
|
+
function agentsDir(): string {
|
|
8
|
+
return path.join(process.cwd(), ".knowhow", "processes", "agents");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the parent taskId for the calling agent.
|
|
13
|
+
*
|
|
14
|
+
* Order of resolution:
|
|
15
|
+
* 1. `_ctx.parentTaskId` if the caller carries it directly
|
|
16
|
+
* 2. `_ctx.caller.parentTaskId` if the agent instance exposes it
|
|
17
|
+
* 3. The `parentTaskId` field of the caller's own metadata.json (looked up by
|
|
18
|
+
* `_ctx.taskId`)
|
|
19
|
+
*/
|
|
20
|
+
function resolveParentTaskId(ctx?: ToolCallContext): string | undefined {
|
|
21
|
+
if (!ctx) return undefined;
|
|
22
|
+
|
|
23
|
+
if (ctx.parentTaskId) return ctx.parentTaskId as string;
|
|
24
|
+
const callerParent = (ctx.caller as any)?.parentTaskId;
|
|
25
|
+
if (callerParent) return callerParent as string;
|
|
26
|
+
|
|
27
|
+
// Fall back to reading our own metadata.json for the parentTaskId field.
|
|
28
|
+
const ownTaskId = ctx.taskId ?? (ctx.caller as any)?.currentTaskId;
|
|
29
|
+
if (!ownTaskId) return undefined;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const metaPath = path.join(agentsDir(), ownTaskId, "metadata.json");
|
|
33
|
+
if (fs.existsSync(metaPath)) {
|
|
34
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
35
|
+
return meta.parentTaskId;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore — no parent metadata available
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Send a message back to the parent agent that spawned this task.
|
|
45
|
+
*
|
|
46
|
+
* This is a self-referential tool: it uses the per-call context (`_ctx`) to
|
|
47
|
+
* discover which agent is calling it and which parent task spawned it, then
|
|
48
|
+
* appends the message to the parent's `input.txt` so the parent picks it up on
|
|
49
|
+
* its next sync cycle.
|
|
50
|
+
*
|
|
51
|
+
* `_ctx` is threaded in by ToolsService.callTool as the LAST positional
|
|
52
|
+
* argument. The agent effectively passes *itself* in via `_ctx.caller`.
|
|
53
|
+
*/
|
|
54
|
+
export async function replyToParent(
|
|
55
|
+
message: string,
|
|
56
|
+
_ctx?: ToolCallContext
|
|
57
|
+
): Promise<string> {
|
|
58
|
+
if (!message || !message.trim()) {
|
|
59
|
+
return "No message provided to replyToParent.";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const parentTaskId = resolveParentTaskId(_ctx);
|
|
63
|
+
if (!parentTaskId) {
|
|
64
|
+
return (
|
|
65
|
+
"No parent task found — this agent was not spawned with a parent taskId, " +
|
|
66
|
+
"so there is no one to reply to."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parentDir = path.join(agentsDir(), parentTaskId);
|
|
71
|
+
const parentInputPath = path.join(parentDir, "input.txt");
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(parentDir)) {
|
|
74
|
+
return `Parent task directory not found: ${parentDir}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fromTaskId =
|
|
78
|
+
_ctx?.taskId ?? (_ctx?.caller as any)?.currentTaskId ?? "unknown";
|
|
79
|
+
const fromAgent = (_ctx?.caller as any)?.name ?? "child";
|
|
80
|
+
|
|
81
|
+
// Envelope with sender identity so the parent knows who is replying.
|
|
82
|
+
const envelope = `[from:${fromTaskId}] [agent:${fromAgent}] ${message}`;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Append rather than overwrite so we don't clobber other pending messages.
|
|
86
|
+
const existing = fs.existsSync(parentInputPath)
|
|
87
|
+
? fs.readFileSync(parentInputPath, "utf8")
|
|
88
|
+
: "";
|
|
89
|
+
const next = existing && !existing.endsWith("\n")
|
|
90
|
+
? `${existing}\n${envelope}\n`
|
|
91
|
+
: `${existing}${envelope}\n`;
|
|
92
|
+
fs.writeFileSync(parentInputPath, next, "utf8");
|
|
93
|
+
} catch (e: any) {
|
|
94
|
+
return `Failed to write to parent input.txt: ${e?.message ?? String(e)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return `Sent message to parent task ${parentTaskId}.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const replyToParentDefinition: Tool = {
|
|
101
|
+
type: "function",
|
|
102
|
+
function: {
|
|
103
|
+
name: "replyToParent",
|
|
104
|
+
description:
|
|
105
|
+
"Send a message back to the parent agent that spawned this task. Use this " +
|
|
106
|
+
"to report progress, ask a question, or alert the parent to something. The " +
|
|
107
|
+
"message is delivered to the parent's input queue and picked up on its next " +
|
|
108
|
+
"sync cycle. Only works when this agent was spawned by another agent (i.e. " +
|
|
109
|
+
"it has a parent taskId).",
|
|
110
|
+
parameters: {
|
|
111
|
+
type: "object",
|
|
112
|
+
positional: true,
|
|
113
|
+
properties: {
|
|
114
|
+
message: {
|
|
115
|
+
type: "string",
|
|
116
|
+
description: "The message to send back to the parent agent",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
required: ["message"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Tool } from "../../clients/types";
|
|
2
|
+
import { GenerationSource } from "../../types";
|
|
3
|
+
import { generate, GenerateOptions, buildWaves } from "../../generate";
|
|
4
|
+
|
|
5
|
+
export interface RunGenerateParams {
|
|
6
|
+
/**
|
|
7
|
+
* In-memory generation sources defining the pipeline. When provided, these
|
|
8
|
+
* are used INSTEAD of the sources in .knowhow/knowhow.json. Each source can
|
|
9
|
+
* declare `dependsOn` for explicit ordering, an `agent` for agent-driven
|
|
10
|
+
* generation, and input/output/prompt for the work itself.
|
|
11
|
+
*/
|
|
12
|
+
sources?: GenerationSource[];
|
|
13
|
+
/** Only run sources whose `name` matches exactly this value (config mode). */
|
|
14
|
+
name?: string;
|
|
15
|
+
/** Only run sources whose name/input/output contains this substring. */
|
|
16
|
+
filter?: string;
|
|
17
|
+
/** Skip hash checks and regenerate all matching sources regardless of changes. */
|
|
18
|
+
force?: boolean;
|
|
19
|
+
/** Max sources to run in parallel within a dependency wave. Default: 3. */
|
|
20
|
+
concurrency?: number;
|
|
21
|
+
/**
|
|
22
|
+
* When true, agent-driven sources create .knowhow/processes/agents/<taskId>/
|
|
23
|
+
* so they appear in `knowhow agents list`.
|
|
24
|
+
*/
|
|
25
|
+
syncFs?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* When true, agent-driven sources push their work to a remote Knowhow task
|
|
28
|
+
* (identified by the source's taskId).
|
|
29
|
+
*/
|
|
30
|
+
syncRemote?: boolean;
|
|
31
|
+
/** When true, only compute and return the execution plan (waves) without running. */
|
|
32
|
+
planOnly?: boolean;
|
|
33
|
+
/** Per-call context injected by ToolsService.callTool. */
|
|
34
|
+
_ctx?: { caller?: any; taskId?: string; [key: string]: any };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* runGenerate — expose `knowhow generate` as an agent tool.
|
|
39
|
+
*
|
|
40
|
+
* This lets an agent orchestrate complex, multi-step pipelines at runtime
|
|
41
|
+
* without needing them defined in the config. It supports:
|
|
42
|
+
* - `dependsOn` for explicit ordering (topological waves)
|
|
43
|
+
* - automatic I/O overlap detection (source B reading source A's output runs after A)
|
|
44
|
+
* - per-source `agent` for agent-driven generation (map/reduce fan-out)
|
|
45
|
+
* - concurrency control within each dependency wave
|
|
46
|
+
* - plain summarization/analysis sources (no agent)
|
|
47
|
+
*
|
|
48
|
+
* With `planOnly: true`, it returns the computed wave plan so the agent can
|
|
49
|
+
* inspect ordering/parallelism before committing to a run.
|
|
50
|
+
*/
|
|
51
|
+
export async function runGenerate(params: RunGenerateParams): Promise<string> {
|
|
52
|
+
const {
|
|
53
|
+
sources,
|
|
54
|
+
name,
|
|
55
|
+
filter,
|
|
56
|
+
force,
|
|
57
|
+
concurrency,
|
|
58
|
+
syncFs,
|
|
59
|
+
syncRemote,
|
|
60
|
+
planOnly,
|
|
61
|
+
} = params ?? {};
|
|
62
|
+
|
|
63
|
+
// Default filesystem sync ON for agent-driven sources so they show up in
|
|
64
|
+
// `knowhow agents list` and can be attached/tailed. Pass syncFs: false to opt out.
|
|
65
|
+
const effectiveSyncFs = syncFs !== false;
|
|
66
|
+
|
|
67
|
+
const options: GenerateOptions = {
|
|
68
|
+
sources,
|
|
69
|
+
name,
|
|
70
|
+
filter,
|
|
71
|
+
force,
|
|
72
|
+
concurrency,
|
|
73
|
+
syncFs: effectiveSyncFs,
|
|
74
|
+
syncRemote,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Resolve the source set for planning output.
|
|
78
|
+
const effectiveSources = sources ?? [];
|
|
79
|
+
|
|
80
|
+
if (planOnly) {
|
|
81
|
+
if (effectiveSources.length === 0) {
|
|
82
|
+
return (
|
|
83
|
+
"planOnly requires in-memory `sources` to build a plan. " +
|
|
84
|
+
"Provide sources to see how they'd be grouped into dependency waves."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const filtered = effectiveSources.filter((s) => {
|
|
88
|
+
if (name) return s.name === name;
|
|
89
|
+
if (filter) {
|
|
90
|
+
const needle = filter.toLowerCase();
|
|
91
|
+
return [s.name, s.input, s.output]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.some((v) => v!.toLowerCase().includes(needle));
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
});
|
|
97
|
+
const waves = buildWaves(filtered);
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
lines.push(
|
|
100
|
+
`📋 Generation Plan (concurrency: ${concurrency ?? 3}, ${filtered.length} sources, ${waves.length} wave${waves.length !== 1 ? "s" : ""})`
|
|
101
|
+
);
|
|
102
|
+
waves.forEach((wave, i) => {
|
|
103
|
+
lines.push(`\n┌─ Wave ${i + 1}/${waves.length} (${wave.length} in parallel)`);
|
|
104
|
+
for (const s of wave) {
|
|
105
|
+
const label = s.name || "(unnamed)";
|
|
106
|
+
const io = `${s.input || "-"} → ${s.output || "-"}`;
|
|
107
|
+
const agentPart = s.agent ? ` [agent: ${s.agent}]` : "";
|
|
108
|
+
lines.push(`│ • ${label} ${io}${agentPart}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push("└────────────────────────────────────────────────");
|
|
111
|
+
});
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await generate(options);
|
|
117
|
+
} catch (e: any) {
|
|
118
|
+
return `Generation failed: ${e?.message ?? String(e)}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const count = effectiveSources.length;
|
|
122
|
+
const mode = count > 0 ? `${count} in-memory source(s)` : "config sources";
|
|
123
|
+
return (
|
|
124
|
+
`Generation complete for ${mode}` +
|
|
125
|
+
(name ? ` (name="${name}")` : filter ? ` (filter="${filter}")` : "") +
|
|
126
|
+
`. Outputs written to their configured paths. ` +
|
|
127
|
+
`If agent-driven sources used syncFs, inspect them with \`knowhow agents list\`.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const runGenerateDefinition: Tool = {
|
|
132
|
+
type: "function",
|
|
133
|
+
function: {
|
|
134
|
+
name: "runGenerate",
|
|
135
|
+
description:
|
|
136
|
+
"Run the `knowhow generate` pipeline. Accepts in-memory `sources` so you can define arbitrary " +
|
|
137
|
+
"task orchestration at runtime WITHOUT editing the config file. Supports dependency ordering via " +
|
|
138
|
+
"`dependsOn`, automatic I/O overlap detection (a source reading another's output runs after it), " +
|
|
139
|
+
"per-source `agent` for agent-driven generation (map/reduce fan-out — one agent per input), " +
|
|
140
|
+
"concurrency control within each dependency wave, and plain summarization/analysis sources. " +
|
|
141
|
+
"Use `planOnly: true` to preview how sources group into concurrent dependency waves before running. " +
|
|
142
|
+
"When `sources` is omitted, it runs the sources defined in .knowhow/knowhow.json (optionally scoped by name/filter).",
|
|
143
|
+
parameters: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
sources: {
|
|
147
|
+
type: "array",
|
|
148
|
+
description:
|
|
149
|
+
"In-memory generation sources. Each item: { name?, input, output, prompt, agent?, model?, " +
|
|
150
|
+
"kind?, outputExt?, outputName?, dependsOn?: string[], syncFs?, maxTimeLimit?, maxSpendLimit? }. " +
|
|
151
|
+
"`input` is a glob or comma-separated file list; `output` is the target file/dir; `prompt` is the " +
|
|
152
|
+
"generation instruction; `agent` (optional) runs an agent per input instead of a summarization call; " +
|
|
153
|
+
"`dependsOn` lists names of sources that must finish first.",
|
|
154
|
+
items: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
name: { type: "string" },
|
|
158
|
+
input: { type: "string" },
|
|
159
|
+
output: { type: "string" },
|
|
160
|
+
prompt: { type: "string" },
|
|
161
|
+
agent: { type: "string" },
|
|
162
|
+
model: { type: "string" },
|
|
163
|
+
kind: { type: "string" },
|
|
164
|
+
outputExt: { type: "string" },
|
|
165
|
+
outputName: { type: "string" },
|
|
166
|
+
dependsOn: { type: "array", items: { type: "string" } },
|
|
167
|
+
syncFs: { type: "boolean" },
|
|
168
|
+
maxTimeLimit: { type: "number" },
|
|
169
|
+
maxSpendLimit: { type: "number" },
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
name: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "Only run sources whose `name` matches exactly (config mode).",
|
|
176
|
+
},
|
|
177
|
+
filter: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "Only run sources whose name/input/output contains this substring.",
|
|
180
|
+
},
|
|
181
|
+
force: {
|
|
182
|
+
type: "boolean",
|
|
183
|
+
description: "Skip hash checks and regenerate all matching sources.",
|
|
184
|
+
},
|
|
185
|
+
concurrency: {
|
|
186
|
+
type: "number",
|
|
187
|
+
description: "Max sources to run in parallel within a dependency wave. Default: 3.",
|
|
188
|
+
},
|
|
189
|
+
syncFs: {
|
|
190
|
+
type: "boolean",
|
|
191
|
+
description:
|
|
192
|
+
"For agent-driven sources, create .knowhow/processes/agents/<taskId>/ so they appear in `knowhow agents list`. " +
|
|
193
|
+
"Defaults to true (enabled). Pass false to opt out.",
|
|
194
|
+
},
|
|
195
|
+
syncRemote: {
|
|
196
|
+
type: "boolean",
|
|
197
|
+
description:
|
|
198
|
+
"For agent-driven sources, push their work to a remote Knowhow task (identified by the source's taskId), " +
|
|
199
|
+
"in addition to fs sync.",
|
|
200
|
+
},
|
|
201
|
+
planOnly: {
|
|
202
|
+
type: "boolean",
|
|
203
|
+
description: "Return the wave execution plan without running anything (requires `sources`).",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
required: [],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { Tool } from "../../clients/types";
|
|
4
|
+
import { ToolCallContext } from "../../services/Tools";
|
|
5
|
+
|
|
6
|
+
/** Resolve the agents dir lazily so tests (and cwd changes) are respected. */
|
|
7
|
+
function agentsDir(): string {
|
|
8
|
+
return path.join(process.cwd(), ".knowhow", "processes", "agents");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Append a line to a task's input.txt without clobbering other pending
|
|
13
|
+
* messages. AgentSyncFs.checkForChanges reads input.txt and turns non-slash
|
|
14
|
+
* content into a pending user message on the target agent (and honors /poke to
|
|
15
|
+
* interrupt).
|
|
16
|
+
*/
|
|
17
|
+
function appendToInput(inputPath: string, line: string): void {
|
|
18
|
+
const existing = fs.existsSync(inputPath)
|
|
19
|
+
? fs.readFileSync(inputPath, "utf8")
|
|
20
|
+
: "";
|
|
21
|
+
const next =
|
|
22
|
+
existing && !existing.endsWith("\n")
|
|
23
|
+
? `${existing}\n${line}\n`
|
|
24
|
+
: `${existing}${line}\n`;
|
|
25
|
+
fs.writeFileSync(inputPath, next, "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SendAgentMessageParams {
|
|
29
|
+
/** The taskId of the agent to send a message to. */
|
|
30
|
+
taskId: string;
|
|
31
|
+
/** The message body to deliver. */
|
|
32
|
+
message: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional sender identifier. If omitted, resolved from the calling agent's
|
|
35
|
+
* own taskId via _ctx. Included as a `[from:...]` envelope so the recipient
|
|
36
|
+
* knows who to reply to.
|
|
37
|
+
*/
|
|
38
|
+
from?: string;
|
|
39
|
+
/**
|
|
40
|
+
* When true, deliver as a `/poke` (interrupts the recipient's current step)
|
|
41
|
+
* rather than a queued user message. Default false.
|
|
42
|
+
*/
|
|
43
|
+
poke?: boolean;
|
|
44
|
+
_ctx?: ToolCallContext;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Send a message to another running agent (a child, a peer, or the parent) by
|
|
49
|
+
* appending it to that agent's input.txt. This is the "down/lateral edge" of
|
|
50
|
+
* agent-to-agent communication — the complement to replyToParent (the up edge).
|
|
51
|
+
*
|
|
52
|
+
* The recipient's AgentSyncFs picks up the message on its next sync cycle and
|
|
53
|
+
* adds it as a pending user message. If `poke` is true, it is delivered as a
|
|
54
|
+
* `/poke` command which interrupts the recipient's current step.
|
|
55
|
+
*
|
|
56
|
+
* Self-referential: the sender identity is resolved from `_ctx` (the calling
|
|
57
|
+
* agent) when `from` isn't supplied.
|
|
58
|
+
*/
|
|
59
|
+
export async function sendAgentMessage(
|
|
60
|
+
params: SendAgentMessageParams
|
|
61
|
+
): Promise<string> {
|
|
62
|
+
const { taskId, message, from, poke = false, _ctx } = params;
|
|
63
|
+
|
|
64
|
+
if (!taskId) {
|
|
65
|
+
return "sendAgentMessage: taskId is required.";
|
|
66
|
+
}
|
|
67
|
+
if (!message || !message.trim()) {
|
|
68
|
+
return "sendAgentMessage: message is required.";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const targetDir = path.join(agentsDir(), taskId);
|
|
72
|
+
const inputPath = path.join(targetDir, "input.txt");
|
|
73
|
+
|
|
74
|
+
if (!fs.existsSync(targetDir)) {
|
|
75
|
+
return `sendAgentMessage: target task directory not found: ${targetDir}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fromId =
|
|
79
|
+
from ??
|
|
80
|
+
_ctx?.taskId ??
|
|
81
|
+
(_ctx?.caller as any)?.currentTaskId ??
|
|
82
|
+
"unknown";
|
|
83
|
+
const fromAgent = (_ctx?.caller as any)?.name;
|
|
84
|
+
const fromLabel = fromAgent ? `${fromId}/${fromAgent}` : fromId;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (poke) {
|
|
88
|
+
// /poke [message] — interrupts the recipient's current step.
|
|
89
|
+
appendToInput(inputPath, `/poke [from:${fromLabel}] ${message}`);
|
|
90
|
+
} else {
|
|
91
|
+
appendToInput(inputPath, `[from:${fromLabel}] ${message}`);
|
|
92
|
+
}
|
|
93
|
+
} catch (e: any) {
|
|
94
|
+
return `sendAgentMessage: failed to write to ${inputPath}: ${
|
|
95
|
+
e?.message ?? String(e)
|
|
96
|
+
}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return `Sent message to task ${taskId}${poke ? " (as /poke)" : ""}.`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const sendAgentMessageDefinition: Tool = {
|
|
103
|
+
type: "function",
|
|
104
|
+
function: {
|
|
105
|
+
name: "sendAgentMessage",
|
|
106
|
+
description:
|
|
107
|
+
"Send a message to another running agent (a child you spawned, a peer, or your parent) by " +
|
|
108
|
+
"appending it to that agent's input queue. The recipient picks it up on its next sync cycle " +
|
|
109
|
+
"as a user message. This is the down/lateral communication edge (complement to replyToParent). " +
|
|
110
|
+
"Set poke:true to interrupt the recipient's current step (delivered as /poke). Your sender " +
|
|
111
|
+
"identity is attached automatically so the recipient can reply back to you.",
|
|
112
|
+
parameters: {
|
|
113
|
+
type: "object",
|
|
114
|
+
positional: false,
|
|
115
|
+
properties: {
|
|
116
|
+
taskId: {
|
|
117
|
+
type: "string",
|
|
118
|
+
description: "The taskId of the agent to send the message to.",
|
|
119
|
+
},
|
|
120
|
+
message: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "The message to deliver to the recipient agent.",
|
|
123
|
+
},
|
|
124
|
+
from: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description:
|
|
127
|
+
"Optional sender id. Defaults to the calling agent's own taskId.",
|
|
128
|
+
},
|
|
129
|
+
poke: {
|
|
130
|
+
type: "boolean",
|
|
131
|
+
description:
|
|
132
|
+
"When true, deliver as a /poke that interrupts the recipient's current step. Default false.",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
required: ["taskId", "message"],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
@@ -9,6 +9,11 @@ export interface StartAgentTaskParams {
|
|
|
9
9
|
taskId?: string;
|
|
10
10
|
resume?: boolean;
|
|
11
11
|
prompt: string;
|
|
12
|
+
/**
|
|
13
|
+
* Push this agent's work to a remote Knowhow task identified by taskId.
|
|
14
|
+
* When set (and no messageId), the spawned agent attaches to that remote task.
|
|
15
|
+
*/
|
|
16
|
+
syncRemote?: boolean;
|
|
12
17
|
provider?: string;
|
|
13
18
|
model?: string;
|
|
14
19
|
agentName?: string;
|
|
@@ -16,6 +21,15 @@ export interface StartAgentTaskParams {
|
|
|
16
21
|
maxSpendLimit?: number;
|
|
17
22
|
/** When true, wait for the agent subprocess to exit before resolving (for generate pipelines). */
|
|
18
23
|
waitForCompletion?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The taskId of the parent agent that is spawning this task. Passed to the
|
|
26
|
+
* child via --parent-task-id so the child knows who to report back to. When
|
|
27
|
+
* omitted, it is auto-populated from the per-call context (`_ctx.taskId`) of
|
|
28
|
+
* the spawning agent.
|
|
29
|
+
*/
|
|
30
|
+
parentTaskId?: string;
|
|
31
|
+
/** Per-call context injected by ToolsService.callTool (contains caller + taskId). */
|
|
32
|
+
_ctx?: { caller?: any; taskId?: string; [key: string]: any };
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
const PROCESSES_DIR = path.join(process.cwd(), ".knowhow", "processes");
|
|
@@ -39,6 +53,37 @@ function generateTaskId(prompt: string): string {
|
|
|
39
53
|
return fullId.slice(0, 80);
|
|
40
54
|
}
|
|
41
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A short capabilities note appended to a spawned subagent's prompt so it knows
|
|
58
|
+
* it can use the `knowhow agents` and related CLI commands (via its execCommand
|
|
59
|
+
* tool) to coordinate, inspect, and orchestrate other agents — things it may
|
|
60
|
+
* not have dedicated tools for.
|
|
61
|
+
*/
|
|
62
|
+
function subagentCapabilitiesNote(): string {
|
|
63
|
+
const lines = [
|
|
64
|
+
"",
|
|
65
|
+
"---",
|
|
66
|
+
"SUBAGENT ORCHESTRATION CAPABILITIES:",
|
|
67
|
+
"You are running as a knowhow agent with a synchronized task directory. In addition to your tools,",
|
|
68
|
+
"you can shell out (via execCommand) to the `knowhow agents` CLI to coordinate and inspect other agents:",
|
|
69
|
+
" • knowhow agents list — list running/recent agent tasks (with row indexes)",
|
|
70
|
+
" • knowhow agents status <id|-i N> — status, cost, last tool/message for a task",
|
|
71
|
+
" • knowhow agents tail <id|-i N> [-f] — read recent messages (read-only; -f to follow live)",
|
|
72
|
+
" • knowhow agents answer <id|-i N> — print the final answer of a COMPLETED task",
|
|
73
|
+
" • knowhow agents attach <id|-i N> — open the interactive chat attached to a running task",
|
|
74
|
+
"You can spawn more subagents with the startAgentTask tool, or run whole pipelines with the runGenerate tool",
|
|
75
|
+
"(supports dependsOn ordering, per-source agents, and concurrency for map/reduce fan-out).",
|
|
76
|
+
"To coordinate spawned subagents from your tools: use waitForAgentCompleted (join a subagent and",
|
|
77
|
+
"get a structured { status, costUsd, finalAnswer } result) and observe (stream a tool's results",
|
|
78
|
+
"back to yourself on an interval, e.g. observe waitForAgentCompleted or `agents status`).",
|
|
79
|
+
"To communicate with running agents: use sendAgentMessage (send a message or /poke to a child/peer/parent),",
|
|
80
|
+
"replyToParent (report back to whoever spawned you), and connectAgent (wire agents together with an ARRAY",
|
|
81
|
+
"of { listener, speaker } connections — enables bidirectional, pipeline, star, and mesh topologies in one call).",
|
|
82
|
+
];
|
|
83
|
+
lines.push("---", "");
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
42
87
|
/**
|
|
43
88
|
* Creates a chat task in Knowhow based on a message ID and prompt.
|
|
44
89
|
* Spawns the knowhow CLI with the prompt piped via stdin to avoid
|
|
@@ -60,31 +105,50 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
60
105
|
taskId: providedTaskId,
|
|
61
106
|
resume,
|
|
62
107
|
syncFs,
|
|
108
|
+
syncRemote,
|
|
63
109
|
provider,
|
|
64
110
|
model,
|
|
65
111
|
agentName,
|
|
66
112
|
maxTimeLimit,
|
|
67
113
|
maxSpendLimit,
|
|
68
114
|
waitForCompletion,
|
|
115
|
+
parentTaskId: explicitParentTaskId,
|
|
116
|
+
_ctx,
|
|
69
117
|
} = params;
|
|
70
118
|
if (!prompt) {
|
|
71
119
|
throw new Error("prompt is required to create a chat task");
|
|
72
120
|
}
|
|
73
121
|
|
|
122
|
+
// Default filesystem synchronization ON unless the caller explicitly opts out
|
|
123
|
+
// (syncFs: false) or is using a messageId-based web sync. This ensures spawned
|
|
124
|
+
// subagents always appear in `knowhow agents list` and can be attached/tailed.
|
|
125
|
+
const useSyncFs = syncFs !== false && !messageId;
|
|
126
|
+
|
|
74
127
|
// Use provided taskId if given, otherwise generate one from the prompt
|
|
75
128
|
const taskId = providedTaskId ?? generateTaskId(prompt);
|
|
76
129
|
const agentTaskDir = path.join(AGENTS_DIR, taskId);
|
|
77
130
|
|
|
131
|
+
// Determine the parent taskId: explicit param wins, otherwise fall back to
|
|
132
|
+
// the spawning agent's taskId carried in the per-call context (`_ctx`).
|
|
133
|
+
const parentTaskId =
|
|
134
|
+
explicitParentTaskId ?? _ctx?.taskId ?? (_ctx?.caller as any)?.currentTaskId;
|
|
135
|
+
|
|
78
136
|
// Build args array (no shell escaping needed - args are passed directly)
|
|
79
137
|
const args: string[] = ["agent"];
|
|
80
138
|
|
|
81
139
|
if (messageId) {
|
|
82
140
|
args.push("--message-id", messageId);
|
|
83
|
-
} else if (
|
|
141
|
+
} else if (useSyncFs) {
|
|
84
142
|
args.push("--sync-fs");
|
|
85
143
|
}
|
|
144
|
+
// When syncRemote is requested, pass it through so the spawned agent pushes
|
|
145
|
+
// its work to the remote task identified by --task-id rather than staying
|
|
146
|
+
// local-only.
|
|
147
|
+
if (syncRemote) {
|
|
148
|
+
args.push("--sync-remote");
|
|
149
|
+
}
|
|
86
150
|
|
|
87
|
-
if (
|
|
151
|
+
if (useSyncFs || providedTaskId) {
|
|
88
152
|
// Pass --task-id whenever we have a known taskId (syncFs or explicit taskId)
|
|
89
153
|
args.push("--task-id", taskId);
|
|
90
154
|
}
|
|
@@ -108,6 +172,10 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
108
172
|
if (maxSpendLimit !== undefined) {
|
|
109
173
|
args.push("--max-spend-limit", String(maxSpendLimit));
|
|
110
174
|
}
|
|
175
|
+
if (parentTaskId) {
|
|
176
|
+
// Tell the child who spawned it so it can report back to the parent.
|
|
177
|
+
args.push("--parent-task-id", parentTaskId);
|
|
178
|
+
}
|
|
111
179
|
if (resume) {
|
|
112
180
|
// --resume is a boolean flag; task ID is already passed via --task-id above
|
|
113
181
|
args.push("--resume");
|
|
@@ -137,7 +205,12 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
137
205
|
fs.writeSync(fd, `PID: ${pid}\n`);
|
|
138
206
|
|
|
139
207
|
// Write prompt to stdin and close it so the process reads it
|
|
140
|
-
|
|
208
|
+
// Append the subagent orchestration capabilities note (unless resuming — a
|
|
209
|
+
// resumed task already has its full context and we don't want to re-inject).
|
|
210
|
+
const promptWithCaps = resume
|
|
211
|
+
? prompt
|
|
212
|
+
: prompt + subagentCapabilitiesNote();
|
|
213
|
+
child.stdin!.write(promptWithCaps, "utf8");
|
|
141
214
|
child.stdin!.end();
|
|
142
215
|
|
|
143
216
|
return new Promise<string>((resolve) => {
|
|
@@ -153,7 +226,7 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
153
226
|
done(`Failed to start agent: ${String(e)}\nLogs: ${logPath}`);
|
|
154
227
|
});
|
|
155
228
|
|
|
156
|
-
const syncFsNote =
|
|
229
|
+
const syncFsNote = useSyncFs
|
|
157
230
|
? `\nTask ID: ${taskId}\nAgent dir: ${agentTaskDir}\n` +
|
|
158
231
|
`To send agent messages, write to: ${agentTaskDir}/input.txt\n` +
|
|
159
232
|
`To check status, read: ${agentTaskDir}/status.txt\n`
|
|
@@ -201,7 +274,15 @@ export const startAgentTaskDefinition: Tool = {
|
|
|
201
274
|
syncFs: {
|
|
202
275
|
type: "boolean",
|
|
203
276
|
description:
|
|
204
|
-
"
|
|
277
|
+
"Filesystem-based synchronization for the task. Defaults to true (enabled) when no messageId is given, " +
|
|
278
|
+
"so the spawned agent always appears in `knowhow agents list` and can be attached/tailed. Pass false to opt out.",
|
|
279
|
+
},
|
|
280
|
+
syncRemote: {
|
|
281
|
+
type: "boolean",
|
|
282
|
+
description:
|
|
283
|
+
"Push this agent's work to a remote Knowhow task identified by taskId (in addition to fs sync). " +
|
|
284
|
+
"Use this to sync a spawned subagent's progress to an already-created remote task. " +
|
|
285
|
+
"If messageId is given instead, a fresh remote task is created from that message.",
|
|
205
286
|
},
|
|
206
287
|
prompt: {
|
|
207
288
|
type: "string",
|
|
@@ -238,6 +319,12 @@ export const startAgentTaskDefinition: Tool = {
|
|
|
238
319
|
description:
|
|
239
320
|
"Resume a previously started task from where it left off. Must be used together with taskId which identifies the task to resume.",
|
|
240
321
|
},
|
|
322
|
+
parentTaskId: {
|
|
323
|
+
type: "string",
|
|
324
|
+
description:
|
|
325
|
+
"The taskId of the parent that should be notified by this child (e.g. via replyToParent). " +
|
|
326
|
+
"If omitted, it is auto-populated from the spawning agent's own taskId.",
|
|
327
|
+
},
|
|
241
328
|
},
|
|
242
329
|
required: ["prompt"],
|
|
243
330
|
},
|