@symerian/symi 3.0.18 → 3.0.19
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/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/package.json +1 -1
- package/extensions/copilot-proxy/README.md +0 -24
- package/extensions/copilot-proxy/index.ts +0 -154
- package/extensions/copilot-proxy/node_modules/.bin/symi +0 -21
- package/extensions/copilot-proxy/package.json +0 -15
- package/extensions/copilot-proxy/symi.plugin.json +0 -9
- package/extensions/device-pair/index.ts +0 -642
- package/extensions/device-pair/symi.plugin.json +0 -20
- package/extensions/diagnostics-otel/index.ts +0 -15
- package/extensions/diagnostics-otel/node_modules/.bin/acorn +0 -21
- package/extensions/diagnostics-otel/node_modules/.bin/symi +0 -21
- package/extensions/diagnostics-otel/package.json +0 -27
- package/extensions/diagnostics-otel/src/service.test.ts +0 -290
- package/extensions/diagnostics-otel/src/service.ts +0 -666
- package/extensions/diagnostics-otel/symi.plugin.json +0 -8
- package/extensions/google-antigravity-auth/README.md +0 -24
- package/extensions/google-antigravity-auth/index.ts +0 -424
- package/extensions/google-antigravity-auth/node_modules/.bin/symi +0 -21
- package/extensions/google-antigravity-auth/package.json +0 -15
- package/extensions/google-antigravity-auth/symi.plugin.json +0 -9
- package/extensions/google-gemini-cli-auth/README.md +0 -35
- package/extensions/google-gemini-cli-auth/index.ts +0 -75
- package/extensions/google-gemini-cli-auth/node_modules/.bin/symi +0 -21
- package/extensions/google-gemini-cli-auth/oauth.test.ts +0 -162
- package/extensions/google-gemini-cli-auth/oauth.ts +0 -636
- package/extensions/google-gemini-cli-auth/package.json +0 -15
- package/extensions/google-gemini-cli-auth/symi.plugin.json +0 -9
- package/extensions/learning-loop/index.ts +0 -159
- package/extensions/learning-loop/node_modules/.bin/symi +0 -21
- package/extensions/learning-loop/package.json +0 -18
- package/extensions/learning-loop/src/analytics/gateway-methods.ts +0 -230
- package/extensions/learning-loop/src/analytics/metrics-aggregator.ts +0 -153
- package/extensions/learning-loop/src/capture/run-tracker.ts +0 -181
- package/extensions/learning-loop/src/capture/serializer.ts +0 -74
- package/extensions/learning-loop/src/db.ts +0 -583
- package/extensions/learning-loop/src/feedback/explicit-feedback.ts +0 -58
- package/extensions/learning-loop/src/feedback/implicit-signals.ts +0 -89
- package/extensions/learning-loop/src/graph/edge-inference.ts +0 -189
- package/extensions/learning-loop/src/graph/graph-retrieval.ts +0 -144
- package/extensions/learning-loop/src/graph/graph-store.ts +0 -183
- package/extensions/learning-loop/src/hooks.ts +0 -244
- package/extensions/learning-loop/src/injection/cache.ts +0 -73
- package/extensions/learning-loop/src/injection/context-injector.ts +0 -104
- package/extensions/learning-loop/src/injection/prompt-builder.ts +0 -43
- package/extensions/learning-loop/src/learning/embedding-bridge.ts +0 -54
- package/extensions/learning-loop/src/learning/learning-extractor.ts +0 -217
- package/extensions/learning-loop/src/learning/learning-store.ts +0 -158
- package/extensions/learning-loop/src/learning/retrieval.ts +0 -87
- package/extensions/learning-loop/src/math/confidence-intervals.ts +0 -62
- package/extensions/learning-loop/src/math/ewma.ts +0 -51
- package/extensions/learning-loop/src/math/weighted-scorer.ts +0 -42
- package/extensions/learning-loop/src/schema.ts +0 -176
- package/extensions/learning-loop/src/scoring/normalization.ts +0 -32
- package/extensions/learning-loop/src/scoring/quality-engine.ts +0 -78
- package/extensions/learning-loop/src/scoring/signal-extractors.ts +0 -155
- package/extensions/learning-loop/src/test/context-injector.test.ts +0 -142
- package/extensions/learning-loop/src/test/fixes.test.ts +0 -1286
- package/extensions/learning-loop/src/test/graph.test.ts +0 -711
- package/extensions/learning-loop/src/test/integration.test.ts +0 -312
- package/extensions/learning-loop/src/test/learning-store.test.ts +0 -191
- package/extensions/learning-loop/src/test/math.test.ts +0 -148
- package/extensions/learning-loop/src/test/quality-engine.test.ts +0 -231
- package/extensions/learning-loop/src/test/run-tracker.test.ts +0 -143
- package/extensions/learning-loop/src/types.ts +0 -281
- package/extensions/learning-loop/symi.plugin.json +0 -46
- package/extensions/llm-task/README.md +0 -97
- package/extensions/llm-task/index.ts +0 -6
- package/extensions/llm-task/package.json +0 -12
- package/extensions/llm-task/src/llm-task-tool.test.ts +0 -138
- package/extensions/llm-task/src/llm-task-tool.ts +0 -249
- package/extensions/llm-task/symi.plugin.json +0 -21
- package/extensions/memory-lancedb/config.ts +0 -161
- package/extensions/memory-lancedb/index.test.ts +0 -330
- package/extensions/memory-lancedb/index.ts +0 -670
- package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +0 -21
- package/extensions/memory-lancedb/node_modules/.bin/openai +0 -21
- package/extensions/memory-lancedb/node_modules/.bin/symi +0 -21
- package/extensions/memory-lancedb/package.json +0 -20
- package/extensions/memory-lancedb/symi.plugin.json +0 -71
- package/extensions/minimax-portal-auth/README.md +0 -33
- package/extensions/minimax-portal-auth/index.ts +0 -161
- package/extensions/minimax-portal-auth/node_modules/.bin/symi +0 -21
- package/extensions/minimax-portal-auth/oauth.ts +0 -247
- package/extensions/minimax-portal-auth/package.json +0 -15
- package/extensions/minimax-portal-auth/symi.plugin.json +0 -9
- package/extensions/model-equalizer/index.ts +0 -80
- package/extensions/model-equalizer/skills/model-equalizer/SKILL.md +0 -58
- package/extensions/model-equalizer/src/detection.ts +0 -62
- package/extensions/model-equalizer/src/enhancer.ts +0 -63
- package/extensions/model-equalizer/src/test/detection.test.ts +0 -218
- package/extensions/model-equalizer/src/test/enhancer.test.ts +0 -137
- package/extensions/model-equalizer/src/test/integration.test.ts +0 -185
- package/extensions/model-equalizer/src/types.ts +0 -24
- package/extensions/model-equalizer/symi.plugin.json +0 -12
- package/extensions/phone-control/index.ts +0 -421
- package/extensions/phone-control/symi.plugin.json +0 -10
- package/extensions/pipeline/README.md +0 -75
- package/extensions/pipeline/SKILL.md +0 -97
- package/extensions/pipeline/index.ts +0 -18
- package/extensions/pipeline/package.json +0 -11
- package/extensions/pipeline/src/pipeline-tool.test.ts +0 -345
- package/extensions/pipeline/src/pipeline-tool.ts +0 -266
- package/extensions/pipeline/src/windows-spawn.test.ts +0 -148
- package/extensions/pipeline/src/windows-spawn.ts +0 -193
- package/extensions/pipeline/symi.plugin.json +0 -10
- package/extensions/qwen-portal-auth/README.md +0 -24
- package/extensions/qwen-portal-auth/index.ts +0 -134
- package/extensions/qwen-portal-auth/oauth.ts +0 -190
- package/extensions/qwen-portal-auth/symi.plugin.json +0 -9
- package/extensions/talk-voice/index.ts +0 -150
- package/extensions/talk-voice/symi.plugin.json +0 -10
- package/extensions/thread-ownership/index.test.ts +0 -180
- package/extensions/thread-ownership/index.ts +0 -133
- package/extensions/thread-ownership/symi.plugin.json +0 -28
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# LLM Task (plugin)
|
|
2
|
-
|
|
3
|
-
Adds an **optional** agent tool `llm-task` for running **JSON-only** LLM tasks
|
|
4
|
-
(drafting, summarizing, classifying) with optional JSON Schema validation.
|
|
5
|
-
|
|
6
|
-
Designed to be called from workflow engines (for example, Pipeline via
|
|
7
|
-
`symi.invoke --each`) without adding new Symi code per workflow.
|
|
8
|
-
|
|
9
|
-
## Enable
|
|
10
|
-
|
|
11
|
-
1. Enable the plugin:
|
|
12
|
-
|
|
13
|
-
```json
|
|
14
|
-
{
|
|
15
|
-
"plugins": {
|
|
16
|
-
"entries": {
|
|
17
|
-
"llm-task": { "enabled": true }
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
2. Allowlist the tool (it is registered with `optional: true`):
|
|
24
|
-
|
|
25
|
-
```json
|
|
26
|
-
{
|
|
27
|
-
"agents": {
|
|
28
|
-
"list": [
|
|
29
|
-
{
|
|
30
|
-
"id": "main",
|
|
31
|
-
"tools": { "allow": ["llm-task"] }
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Config (optional)
|
|
39
|
-
|
|
40
|
-
```json
|
|
41
|
-
{
|
|
42
|
-
"plugins": {
|
|
43
|
-
"entries": {
|
|
44
|
-
"llm-task": {
|
|
45
|
-
"enabled": true,
|
|
46
|
-
"config": {
|
|
47
|
-
"defaultProvider": "openai-codex",
|
|
48
|
-
"defaultModel": "gpt-5.2",
|
|
49
|
-
"defaultAuthProfileId": "main",
|
|
50
|
-
"allowedModels": ["openai-codex/gpt-5.2"],
|
|
51
|
-
"maxTokens": 800,
|
|
52
|
-
"timeoutMs": 30000
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`allowedModels` is an allowlist of `provider/model` strings. If set, any request
|
|
61
|
-
outside the list is rejected.
|
|
62
|
-
|
|
63
|
-
## Tool API
|
|
64
|
-
|
|
65
|
-
### Parameters
|
|
66
|
-
|
|
67
|
-
- `prompt` (string, required)
|
|
68
|
-
- `input` (any, optional)
|
|
69
|
-
- `schema` (object, optional JSON Schema)
|
|
70
|
-
- `provider` (string, optional)
|
|
71
|
-
- `model` (string, optional)
|
|
72
|
-
- `authProfileId` (string, optional)
|
|
73
|
-
- `temperature` (number, optional)
|
|
74
|
-
- `maxTokens` (number, optional)
|
|
75
|
-
- `timeoutMs` (number, optional)
|
|
76
|
-
|
|
77
|
-
### Output
|
|
78
|
-
|
|
79
|
-
Returns `details.json` containing the parsed JSON (and validates against
|
|
80
|
-
`schema` when provided).
|
|
81
|
-
|
|
82
|
-
## Notes
|
|
83
|
-
|
|
84
|
-
- The tool is **JSON-only** and instructs the model to output only JSON
|
|
85
|
-
(no code fences, no commentary).
|
|
86
|
-
- No tools are exposed to the model for this run.
|
|
87
|
-
- Side effects should be handled outside this tool (for example, approvals in
|
|
88
|
-
Pipeline) before calling tools that send messages/emails.
|
|
89
|
-
|
|
90
|
-
## Bundled extension note
|
|
91
|
-
|
|
92
|
-
This extension depends on Symi internal modules (the embedded agent runner).
|
|
93
|
-
It is intended to ship as a **bundled** Symi extension (like `pipeline`) and
|
|
94
|
-
be enabled via `plugins.entries` + tool allowlists.
|
|
95
|
-
|
|
96
|
-
It is **not** currently designed to be copied into
|
|
97
|
-
`~/.symi/extensions` as a standalone plugin directory.
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { AnyAgentTool, SymiPluginApi } from "../../src/plugins/types.js";
|
|
2
|
-
import { createLlmTaskTool } from "./src/llm-task-tool.js";
|
|
3
|
-
|
|
4
|
-
export default function register(api: SymiPluginApi) {
|
|
5
|
-
api.registerTool(createLlmTaskTool(api) as unknown as AnyAgentTool, { optional: true });
|
|
6
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
vi.mock("../../../src/agents/pi-embedded-runner.js", () => {
|
|
4
|
-
return {
|
|
5
|
-
runEmbeddedPiAgent: vi.fn(async () => ({
|
|
6
|
-
meta: { startedAt: Date.now() },
|
|
7
|
-
payloads: [{ text: "{}" }],
|
|
8
|
-
})),
|
|
9
|
-
};
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
import { runEmbeddedPiAgent } from "../../../src/agents/pi-embedded-runner.js";
|
|
13
|
-
import { createLlmTaskTool } from "./llm-task-tool.js";
|
|
14
|
-
|
|
15
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
16
|
-
function fakeApi(overrides: any = {}) {
|
|
17
|
-
return {
|
|
18
|
-
id: "llm-task",
|
|
19
|
-
name: "llm-task",
|
|
20
|
-
source: "test",
|
|
21
|
-
config: {
|
|
22
|
-
agents: { defaults: { workspace: "/tmp", model: { primary: "openai-codex/gpt-5.2" } } },
|
|
23
|
-
},
|
|
24
|
-
pluginConfig: {},
|
|
25
|
-
runtime: { version: "test" },
|
|
26
|
-
logger: { debug() {}, info() {}, warn() {}, error() {} },
|
|
27
|
-
registerTool() {},
|
|
28
|
-
...overrides,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("llm-task tool (json-only)", () => {
|
|
33
|
-
beforeEach(() => vi.clearAllMocks());
|
|
34
|
-
|
|
35
|
-
it("returns parsed json", async () => {
|
|
36
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
37
|
-
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
38
|
-
meta: {},
|
|
39
|
-
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
|
40
|
-
});
|
|
41
|
-
const tool = createLlmTaskTool(fakeApi());
|
|
42
|
-
const res = await tool.execute("id", { prompt: "return foo" });
|
|
43
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
44
|
-
expect((res as any).details.json).toEqual({ foo: "bar" });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("strips fenced json", async () => {
|
|
48
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
49
|
-
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
50
|
-
meta: {},
|
|
51
|
-
payloads: [{ text: '```json\n{"ok":true}\n```' }],
|
|
52
|
-
});
|
|
53
|
-
const tool = createLlmTaskTool(fakeApi());
|
|
54
|
-
const res = await tool.execute("id", { prompt: "return ok" });
|
|
55
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
56
|
-
expect((res as any).details.json).toEqual({ ok: true });
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("validates schema", async () => {
|
|
60
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
61
|
-
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
62
|
-
meta: {},
|
|
63
|
-
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
|
64
|
-
});
|
|
65
|
-
const tool = createLlmTaskTool(fakeApi());
|
|
66
|
-
const schema = {
|
|
67
|
-
type: "object",
|
|
68
|
-
properties: { foo: { type: "string" } },
|
|
69
|
-
required: ["foo"],
|
|
70
|
-
additionalProperties: false,
|
|
71
|
-
};
|
|
72
|
-
const res = await tool.execute("id", { prompt: "return foo", schema });
|
|
73
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
74
|
-
expect((res as any).details.json).toEqual({ foo: "bar" });
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("throws on invalid json", async () => {
|
|
78
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
79
|
-
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
80
|
-
meta: {},
|
|
81
|
-
payloads: [{ text: "not-json" }],
|
|
82
|
-
});
|
|
83
|
-
const tool = createLlmTaskTool(fakeApi());
|
|
84
|
-
await expect(tool.execute("id", { prompt: "x" })).rejects.toThrow(/invalid json/i);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("throws on schema mismatch", async () => {
|
|
88
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
89
|
-
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
90
|
-
meta: {},
|
|
91
|
-
payloads: [{ text: JSON.stringify({ foo: 1 }) }],
|
|
92
|
-
});
|
|
93
|
-
const tool = createLlmTaskTool(fakeApi());
|
|
94
|
-
const schema = { type: "object", properties: { foo: { type: "string" } }, required: ["foo"] };
|
|
95
|
-
await expect(tool.execute("id", { prompt: "x", schema })).rejects.toThrow(/match schema/i);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("passes provider/model overrides to embedded runner", async () => {
|
|
99
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
100
|
-
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
101
|
-
meta: {},
|
|
102
|
-
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
|
103
|
-
});
|
|
104
|
-
const tool = createLlmTaskTool(fakeApi());
|
|
105
|
-
await tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" });
|
|
106
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
107
|
-
const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0];
|
|
108
|
-
expect(call.provider).toBe("anthropic");
|
|
109
|
-
expect(call.model).toBe("claude-4-sonnet");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("enforces allowedModels", async () => {
|
|
113
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
114
|
-
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
115
|
-
meta: {},
|
|
116
|
-
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
|
117
|
-
});
|
|
118
|
-
const tool = createLlmTaskTool(
|
|
119
|
-
fakeApi({ pluginConfig: { allowedModels: ["openai-codex/gpt-5.2"] } }),
|
|
120
|
-
);
|
|
121
|
-
await expect(
|
|
122
|
-
tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" }),
|
|
123
|
-
).rejects.toThrow(/not allowed/i);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("disables tools for embedded run", async () => {
|
|
127
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
128
|
-
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
|
|
129
|
-
meta: {},
|
|
130
|
-
payloads: [{ text: JSON.stringify({ ok: true }) }],
|
|
131
|
-
});
|
|
132
|
-
const tool = createLlmTaskTool(fakeApi());
|
|
133
|
-
await tool.execute("id", { prompt: "x" });
|
|
134
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
135
|
-
const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0];
|
|
136
|
-
expect(call.disableTools).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { Type } from "@sinclair/typebox";
|
|
5
|
-
import Ajv from "ajv";
|
|
6
|
-
// NOTE: This extension is intended to be bundled with Symi.
|
|
7
|
-
// When running from source (tests/dev), Symi internals live under src/.
|
|
8
|
-
// When running from a built install, internals live under dist/ (no src/ tree).
|
|
9
|
-
// So we resolve internal imports dynamically with src-first, dist-fallback.
|
|
10
|
-
import type { SymiPluginApi } from "../../../src/plugins/types.js";
|
|
11
|
-
|
|
12
|
-
type RunEmbeddedPiAgentFn = (params: Record<string, unknown>) => Promise<unknown>;
|
|
13
|
-
|
|
14
|
-
async function loadRunEmbeddedPiAgent(): Promise<RunEmbeddedPiAgentFn> {
|
|
15
|
-
// Source checkout (tests/dev)
|
|
16
|
-
try {
|
|
17
|
-
const mod = await import("../../../src/agents/pi-embedded-runner.js");
|
|
18
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
19
|
-
if (typeof (mod as any).runEmbeddedPiAgent === "function") {
|
|
20
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
21
|
-
return (mod as any).runEmbeddedPiAgent;
|
|
22
|
-
}
|
|
23
|
-
} catch {
|
|
24
|
-
// ignore
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Bundled install (built)
|
|
28
|
-
const mod = await import("../../../src/agents/pi-embedded-runner.js");
|
|
29
|
-
if (typeof mod.runEmbeddedPiAgent !== "function") {
|
|
30
|
-
throw new Error("Internal error: runEmbeddedPiAgent not available");
|
|
31
|
-
}
|
|
32
|
-
return mod.runEmbeddedPiAgent as RunEmbeddedPiAgentFn;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stripCodeFences(s: string): string {
|
|
36
|
-
const trimmed = s.trim();
|
|
37
|
-
const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
38
|
-
if (m) {
|
|
39
|
-
return (m[1] ?? "").trim();
|
|
40
|
-
}
|
|
41
|
-
return trimmed;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function collectText(payloads: Array<{ text?: string; isError?: boolean }> | undefined): string {
|
|
45
|
-
const texts = (payloads ?? [])
|
|
46
|
-
.filter((p) => !p.isError && typeof p.text === "string")
|
|
47
|
-
.map((p) => p.text ?? "");
|
|
48
|
-
return texts.join("\n").trim();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function toModelKey(provider?: string, model?: string): string | undefined {
|
|
52
|
-
const p = provider?.trim();
|
|
53
|
-
const m = model?.trim();
|
|
54
|
-
if (!p || !m) {
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
return `${p}/${m}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
type PluginCfg = {
|
|
61
|
-
defaultProvider?: string;
|
|
62
|
-
defaultModel?: string;
|
|
63
|
-
defaultAuthProfileId?: string;
|
|
64
|
-
allowedModels?: string[];
|
|
65
|
-
maxTokens?: number;
|
|
66
|
-
timeoutMs?: number;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export function createLlmTaskTool(api: SymiPluginApi) {
|
|
70
|
-
return {
|
|
71
|
-
name: "llm-task",
|
|
72
|
-
label: "LLM Task",
|
|
73
|
-
description:
|
|
74
|
-
"Run a generic JSON-only LLM task and return schema-validated JSON. Designed for orchestration from Pipeline workflows via symi.invoke.",
|
|
75
|
-
parameters: Type.Object({
|
|
76
|
-
prompt: Type.String({ description: "Task instruction for the LLM." }),
|
|
77
|
-
input: Type.Optional(Type.Unknown({ description: "Optional input payload for the task." })),
|
|
78
|
-
schema: Type.Optional(
|
|
79
|
-
Type.Unknown({ description: "Optional JSON Schema to validate the returned JSON." }),
|
|
80
|
-
),
|
|
81
|
-
provider: Type.Optional(
|
|
82
|
-
Type.String({ description: "Provider override (e.g. openai-codex, anthropic)." }),
|
|
83
|
-
),
|
|
84
|
-
model: Type.Optional(Type.String({ description: "Model id override." })),
|
|
85
|
-
authProfileId: Type.Optional(Type.String({ description: "Auth profile override." })),
|
|
86
|
-
temperature: Type.Optional(Type.Number({ description: "Best-effort temperature override." })),
|
|
87
|
-
maxTokens: Type.Optional(Type.Number({ description: "Best-effort maxTokens override." })),
|
|
88
|
-
timeoutMs: Type.Optional(Type.Number({ description: "Timeout for the LLM run." })),
|
|
89
|
-
}),
|
|
90
|
-
|
|
91
|
-
async execute(_id: string, params: Record<string, unknown>) {
|
|
92
|
-
const prompt = typeof params.prompt === "string" ? params.prompt : "";
|
|
93
|
-
if (!prompt.trim()) {
|
|
94
|
-
throw new Error("prompt required");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const pluginCfg = (api.pluginConfig ?? {}) as PluginCfg;
|
|
98
|
-
|
|
99
|
-
const primary = api.config?.agents?.defaults?.model?.primary;
|
|
100
|
-
const primaryProvider = typeof primary === "string" ? primary.split("/")[0] : undefined;
|
|
101
|
-
const primaryModel =
|
|
102
|
-
typeof primary === "string" ? primary.split("/").slice(1).join("/") : undefined;
|
|
103
|
-
|
|
104
|
-
const provider =
|
|
105
|
-
(typeof params.provider === "string" && params.provider.trim()) ||
|
|
106
|
-
(typeof pluginCfg.defaultProvider === "string" && pluginCfg.defaultProvider.trim()) ||
|
|
107
|
-
primaryProvider ||
|
|
108
|
-
undefined;
|
|
109
|
-
|
|
110
|
-
const model =
|
|
111
|
-
(typeof params.model === "string" && params.model.trim()) ||
|
|
112
|
-
(typeof pluginCfg.defaultModel === "string" && pluginCfg.defaultModel.trim()) ||
|
|
113
|
-
primaryModel ||
|
|
114
|
-
undefined;
|
|
115
|
-
|
|
116
|
-
const authProfileId =
|
|
117
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
118
|
-
(typeof (params as any).authProfileId === "string" &&
|
|
119
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
120
|
-
(params as any).authProfileId.trim()) ||
|
|
121
|
-
(typeof pluginCfg.defaultAuthProfileId === "string" &&
|
|
122
|
-
pluginCfg.defaultAuthProfileId.trim()) ||
|
|
123
|
-
undefined;
|
|
124
|
-
|
|
125
|
-
const modelKey = toModelKey(provider, model);
|
|
126
|
-
if (!provider || !model || !modelKey) {
|
|
127
|
-
throw new Error(
|
|
128
|
-
`provider/model could not be resolved (provider=${String(provider ?? "")}, model=${String(model ?? "")})`,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const allowed = Array.isArray(pluginCfg.allowedModels) ? pluginCfg.allowedModels : undefined;
|
|
133
|
-
if (allowed && allowed.length > 0 && !allowed.includes(modelKey)) {
|
|
134
|
-
throw new Error(
|
|
135
|
-
`Model not allowed by llm-task plugin config: ${modelKey}. Allowed models: ${allowed.join(", ")}`,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const timeoutMs =
|
|
140
|
-
(typeof params.timeoutMs === "number" && params.timeoutMs > 0
|
|
141
|
-
? params.timeoutMs
|
|
142
|
-
: undefined) ||
|
|
143
|
-
(typeof pluginCfg.timeoutMs === "number" && pluginCfg.timeoutMs > 0
|
|
144
|
-
? pluginCfg.timeoutMs
|
|
145
|
-
: undefined) ||
|
|
146
|
-
30_000;
|
|
147
|
-
|
|
148
|
-
const streamParams = {
|
|
149
|
-
temperature: typeof params.temperature === "number" ? params.temperature : undefined,
|
|
150
|
-
maxTokens:
|
|
151
|
-
typeof params.maxTokens === "number"
|
|
152
|
-
? params.maxTokens
|
|
153
|
-
: typeof pluginCfg.maxTokens === "number"
|
|
154
|
-
? pluginCfg.maxTokens
|
|
155
|
-
: undefined,
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
159
|
-
const input = (params as any).input as unknown;
|
|
160
|
-
let inputJson: string;
|
|
161
|
-
try {
|
|
162
|
-
inputJson = JSON.stringify(input ?? null, null, 2);
|
|
163
|
-
} catch {
|
|
164
|
-
throw new Error("input must be JSON-serializable");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const system = [
|
|
168
|
-
"You are a JSON-only function.",
|
|
169
|
-
"Return ONLY a valid JSON value.",
|
|
170
|
-
"Do not wrap in markdown fences.",
|
|
171
|
-
"Do not include commentary.",
|
|
172
|
-
"Do not call tools.",
|
|
173
|
-
].join(" ");
|
|
174
|
-
|
|
175
|
-
const fullPrompt = `${system}\n\nTASK:\n${prompt}\n\nINPUT_JSON:\n${inputJson}\n`;
|
|
176
|
-
|
|
177
|
-
let tmpDir: string | null = null;
|
|
178
|
-
try {
|
|
179
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "symi-llm-task-"));
|
|
180
|
-
const sessionId = `llm-task-${Date.now()}`;
|
|
181
|
-
const sessionFile = path.join(tmpDir, "session.json");
|
|
182
|
-
|
|
183
|
-
const runEmbeddedPiAgent = await loadRunEmbeddedPiAgent();
|
|
184
|
-
|
|
185
|
-
const result = await runEmbeddedPiAgent({
|
|
186
|
-
sessionId,
|
|
187
|
-
sessionFile,
|
|
188
|
-
workspaceDir: api.config?.agents?.defaults?.workspace ?? process.cwd(),
|
|
189
|
-
config: api.config,
|
|
190
|
-
prompt: fullPrompt,
|
|
191
|
-
timeoutMs,
|
|
192
|
-
runId: `llm-task-${Date.now()}`,
|
|
193
|
-
provider,
|
|
194
|
-
model,
|
|
195
|
-
authProfileId,
|
|
196
|
-
authProfileIdSource: authProfileId ? "user" : "auto",
|
|
197
|
-
streamParams,
|
|
198
|
-
disableTools: true,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
202
|
-
const text = collectText((result as any).payloads);
|
|
203
|
-
if (!text) {
|
|
204
|
-
throw new Error("LLM returned empty output");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const raw = stripCodeFences(text);
|
|
208
|
-
let parsed: unknown;
|
|
209
|
-
try {
|
|
210
|
-
parsed = JSON.parse(raw);
|
|
211
|
-
} catch {
|
|
212
|
-
throw new Error("LLM returned invalid JSON");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
216
|
-
const schema = (params as any).schema as unknown;
|
|
217
|
-
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
|
|
218
|
-
const ajv = new Ajv.default({ allErrors: true, strict: false });
|
|
219
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
220
|
-
const validate = ajv.compile(schema as any);
|
|
221
|
-
const ok = validate(parsed);
|
|
222
|
-
if (!ok) {
|
|
223
|
-
const msg =
|
|
224
|
-
validate.errors
|
|
225
|
-
?.map(
|
|
226
|
-
(e: { instancePath?: string; message?: string }) =>
|
|
227
|
-
`${e.instancePath || "<root>"} ${e.message || "invalid"}`,
|
|
228
|
-
)
|
|
229
|
-
.join("; ") ?? "invalid";
|
|
230
|
-
throw new Error(`LLM JSON did not match schema: ${msg}`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }],
|
|
236
|
-
details: { json: parsed, provider, model },
|
|
237
|
-
};
|
|
238
|
-
} finally {
|
|
239
|
-
if (tmpDir) {
|
|
240
|
-
try {
|
|
241
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
242
|
-
} catch {
|
|
243
|
-
// ignore
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
};
|
|
249
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "llm-task",
|
|
3
|
-
"name": "LLM Task",
|
|
4
|
-
"description": "Generic JSON-only LLM tool for structured tasks callable from workflows.",
|
|
5
|
-
"configSchema": {
|
|
6
|
-
"type": "object",
|
|
7
|
-
"additionalProperties": false,
|
|
8
|
-
"properties": {
|
|
9
|
-
"defaultProvider": { "type": "string" },
|
|
10
|
-
"defaultModel": { "type": "string" },
|
|
11
|
-
"defaultAuthProfileId": { "type": "string" },
|
|
12
|
-
"allowedModels": {
|
|
13
|
-
"type": "array",
|
|
14
|
-
"items": { "type": "string" },
|
|
15
|
-
"description": "Allowlist of provider/model keys like openai-codex/gpt-5.2."
|
|
16
|
-
},
|
|
17
|
-
"maxTokens": { "type": "number" },
|
|
18
|
-
"timeoutMs": { "type": "number" }
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
export type MemoryConfig = {
|
|
6
|
-
embedding: {
|
|
7
|
-
provider: "openai";
|
|
8
|
-
model?: string;
|
|
9
|
-
apiKey: string;
|
|
10
|
-
};
|
|
11
|
-
dbPath?: string;
|
|
12
|
-
autoCapture?: boolean;
|
|
13
|
-
autoRecall?: boolean;
|
|
14
|
-
captureMaxChars?: number;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const MEMORY_CATEGORIES = ["preference", "fact", "decision", "entity", "other"] as const;
|
|
18
|
-
export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number];
|
|
19
|
-
|
|
20
|
-
const DEFAULT_MODEL = "text-embedding-3-small";
|
|
21
|
-
export const DEFAULT_CAPTURE_MAX_CHARS = 500;
|
|
22
|
-
const LEGACY_STATE_DIRS: string[] = [];
|
|
23
|
-
|
|
24
|
-
function resolveDefaultDbPath(): string {
|
|
25
|
-
const home = homedir();
|
|
26
|
-
const preferred = join(home, ".symi", "memory", "lancedb");
|
|
27
|
-
try {
|
|
28
|
-
if (fs.existsSync(preferred)) {
|
|
29
|
-
return preferred;
|
|
30
|
-
}
|
|
31
|
-
} catch {
|
|
32
|
-
// best-effort
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
for (const legacy of LEGACY_STATE_DIRS) {
|
|
36
|
-
const candidate = join(home, legacy, "memory", "lancedb");
|
|
37
|
-
try {
|
|
38
|
-
if (fs.existsSync(candidate)) {
|
|
39
|
-
return candidate;
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
// best-effort
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return preferred;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const DEFAULT_DB_PATH = resolveDefaultDbPath();
|
|
50
|
-
|
|
51
|
-
const EMBEDDING_DIMENSIONS: Record<string, number> = {
|
|
52
|
-
"text-embedding-3-small": 1536,
|
|
53
|
-
"text-embedding-3-large": 3072,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
function assertAllowedKeys(value: Record<string, unknown>, allowed: string[], label: string) {
|
|
57
|
-
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
58
|
-
if (unknown.length === 0) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function vectorDimsForModel(model: string): number {
|
|
65
|
-
const dims = EMBEDDING_DIMENSIONS[model];
|
|
66
|
-
if (!dims) {
|
|
67
|
-
throw new Error(`Unsupported embedding model: ${model}`);
|
|
68
|
-
}
|
|
69
|
-
return dims;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function resolveEnvVars(value: string): string {
|
|
73
|
-
return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
|
|
74
|
-
const envValue = process.env[envVar];
|
|
75
|
-
if (!envValue) {
|
|
76
|
-
throw new Error(`Environment variable ${envVar} is not set`);
|
|
77
|
-
}
|
|
78
|
-
return envValue;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function resolveEmbeddingModel(embedding: Record<string, unknown>): string {
|
|
83
|
-
const model = typeof embedding.model === "string" ? embedding.model : DEFAULT_MODEL;
|
|
84
|
-
vectorDimsForModel(model);
|
|
85
|
-
return model;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export const memoryConfigSchema = {
|
|
89
|
-
parse(value: unknown): MemoryConfig {
|
|
90
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
91
|
-
throw new Error("memory config required");
|
|
92
|
-
}
|
|
93
|
-
const cfg = value as Record<string, unknown>;
|
|
94
|
-
assertAllowedKeys(
|
|
95
|
-
cfg,
|
|
96
|
-
["embedding", "dbPath", "autoCapture", "autoRecall", "captureMaxChars"],
|
|
97
|
-
"memory config",
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const embedding = cfg.embedding as Record<string, unknown> | undefined;
|
|
101
|
-
if (!embedding || typeof embedding.apiKey !== "string") {
|
|
102
|
-
throw new Error("embedding.apiKey is required");
|
|
103
|
-
}
|
|
104
|
-
assertAllowedKeys(embedding, ["apiKey", "model"], "embedding config");
|
|
105
|
-
|
|
106
|
-
const model = resolveEmbeddingModel(embedding);
|
|
107
|
-
|
|
108
|
-
const captureMaxChars =
|
|
109
|
-
typeof cfg.captureMaxChars === "number" ? Math.floor(cfg.captureMaxChars) : undefined;
|
|
110
|
-
if (
|
|
111
|
-
typeof captureMaxChars === "number" &&
|
|
112
|
-
(captureMaxChars < 100 || captureMaxChars > 10_000)
|
|
113
|
-
) {
|
|
114
|
-
throw new Error("captureMaxChars must be between 100 and 10000");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
embedding: {
|
|
119
|
-
provider: "openai",
|
|
120
|
-
model,
|
|
121
|
-
apiKey: resolveEnvVars(embedding.apiKey),
|
|
122
|
-
},
|
|
123
|
-
dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : DEFAULT_DB_PATH,
|
|
124
|
-
autoCapture: cfg.autoCapture === true,
|
|
125
|
-
autoRecall: cfg.autoRecall !== false,
|
|
126
|
-
captureMaxChars: captureMaxChars ?? DEFAULT_CAPTURE_MAX_CHARS,
|
|
127
|
-
};
|
|
128
|
-
},
|
|
129
|
-
uiHints: {
|
|
130
|
-
"embedding.apiKey": {
|
|
131
|
-
label: "OpenAI API Key",
|
|
132
|
-
sensitive: true,
|
|
133
|
-
placeholder: "sk-proj-...",
|
|
134
|
-
help: "API key for OpenAI embeddings (or use ${OPENAI_API_KEY})",
|
|
135
|
-
},
|
|
136
|
-
"embedding.model": {
|
|
137
|
-
label: "Embedding Model",
|
|
138
|
-
placeholder: DEFAULT_MODEL,
|
|
139
|
-
help: "OpenAI embedding model to use",
|
|
140
|
-
},
|
|
141
|
-
dbPath: {
|
|
142
|
-
label: "Database Path",
|
|
143
|
-
placeholder: "~/.symi/memory/lancedb",
|
|
144
|
-
advanced: true,
|
|
145
|
-
},
|
|
146
|
-
autoCapture: {
|
|
147
|
-
label: "Auto-Capture",
|
|
148
|
-
help: "Automatically capture important information from conversations",
|
|
149
|
-
},
|
|
150
|
-
autoRecall: {
|
|
151
|
-
label: "Auto-Recall",
|
|
152
|
-
help: "Automatically inject relevant memories into context",
|
|
153
|
-
},
|
|
154
|
-
captureMaxChars: {
|
|
155
|
-
label: "Capture Max Chars",
|
|
156
|
-
help: "Maximum message length eligible for auto-capture",
|
|
157
|
-
advanced: true,
|
|
158
|
-
placeholder: String(DEFAULT_CAPTURE_MAX_CHARS),
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
};
|