negotium 0.2.8 → 0.2.10
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/agent-helpers.js +21 -20
- package/dist/agent-helpers.js.map +6 -6
- package/dist/background-bash.js +2 -1
- package/dist/background-bash.js.map +3 -3
- package/dist/browser-runtime.js +2 -1
- package/dist/browser-runtime.js.map +3 -3
- package/dist/{chunk-knv045mn.js → chunk-dcfmsrnb.js} +2 -1
- package/dist/{chunk-knv045mn.js.map → chunk-dcfmsrnb.js.map} +3 -3
- package/dist/hosted-agent.js +3 -2
- package/dist/hosted-agent.js.map +4 -4
- package/dist/main.js +522 -269
- package/dist/main.js.map +11 -9
- package/dist/mcp-factories.js +21 -20
- package/dist/mcp-factories.js.map +6 -6
- package/dist/prompts.js +2 -1
- package/dist/prompts.js.map +3 -3
- package/dist/query-runtime.js +2 -1
- package/dist/query-runtime.js.map +3 -3
- package/dist/registry.js +9 -5
- package/dist/registry.js.map +4 -4
- package/dist/rollout.js +1 -1
- package/dist/runtime/src/agents/maestro-registry.ts +32 -2
- package/dist/runtime/src/agents/mcp-tools/inline-assets.ts +193 -0
- package/dist/runtime/src/agents/mcp-tools/publish-html.ts +183 -0
- package/dist/runtime/src/mcp-runtime-host.ts +1 -0
- package/dist/runtime/src/platform/config.ts +12 -0
- package/dist/runtime/src/runtime/turn-runner.ts +34 -11
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +2 -1
- package/dist/runtime-helpers.js.map +3 -3
- package/dist/types/packages/core/src/platform/config.d.ts +7 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js +2 -1
- package/dist/vault.js.map +3 -3
- package/package.json +2 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline local file references in an HTML document as data URIs.
|
|
3
|
+
*
|
|
4
|
+
* A published snippet is a single file with no asset directory, so a
|
|
5
|
+
* generated document that points at `./chart.png` would render a broken
|
|
6
|
+
* image. Rewriting those references into `data:` URIs keeps the snippet
|
|
7
|
+
* self-contained without giving the store a second lifecycle to manage.
|
|
8
|
+
*
|
|
9
|
+
* Only references that resolve to an existing local file are touched.
|
|
10
|
+
* Remote URLs, `data:`/`blob:` URIs, protocol-relative URLs and fragments
|
|
11
|
+
* are left exactly as written.
|
|
12
|
+
*
|
|
13
|
+
* The scanner is regex-based rather than a full HTML parse. That is a
|
|
14
|
+
* deliberate tradeoff for machine-generated documents: it handles the
|
|
15
|
+
* attribute shapes models actually emit, and anything it fails to match is
|
|
16
|
+
* simply left alone rather than corrupted.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
19
|
+
import { extname, isAbsolute, resolve } from "node:path";
|
|
20
|
+
|
|
21
|
+
const MIME_BY_EXT: Record<string, string> = {
|
|
22
|
+
".apng": "image/apng",
|
|
23
|
+
".avif": "image/avif",
|
|
24
|
+
".bmp": "image/bmp",
|
|
25
|
+
".css": "text/css",
|
|
26
|
+
".gif": "image/gif",
|
|
27
|
+
".ico": "image/x-icon",
|
|
28
|
+
".jpeg": "image/jpeg",
|
|
29
|
+
".jpg": "image/jpeg",
|
|
30
|
+
".mp3": "audio/mpeg",
|
|
31
|
+
".mp4": "video/mp4",
|
|
32
|
+
".otf": "font/otf",
|
|
33
|
+
".png": "image/png",
|
|
34
|
+
".svg": "image/svg+xml",
|
|
35
|
+
".ttf": "font/ttf",
|
|
36
|
+
".wav": "audio/wav",
|
|
37
|
+
".webm": "video/webm",
|
|
38
|
+
".webp": "image/webp",
|
|
39
|
+
".woff": "font/woff",
|
|
40
|
+
".woff2": "font/woff2",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface InlineOptions {
|
|
44
|
+
/** Directory that relative references resolve against. */
|
|
45
|
+
baseDir: string;
|
|
46
|
+
/** Skip any single asset larger than this. */
|
|
47
|
+
maxAssetBytes: number;
|
|
48
|
+
/** Stop inlining once the document would exceed this. */
|
|
49
|
+
maxTotalBytes: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface InlineReport {
|
|
53
|
+
html: string;
|
|
54
|
+
inlined: { ref: string; bytes: number }[];
|
|
55
|
+
/** Looked like a local file but nothing was there — likely a broken image. */
|
|
56
|
+
unresolved: string[];
|
|
57
|
+
/** Existed but was left as-is; the document would have grown too large. */
|
|
58
|
+
skipped: { ref: string; bytes: number; reason: "asset-too-large" | "document-too-large" }[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Remote URL, data/blob URI, protocol-relative URL, or bare fragment. */
|
|
62
|
+
function isNonLocal(ref: string): boolean {
|
|
63
|
+
return /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(ref.trim());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `./chart.png?v=2#frag` → `./chart.png` */
|
|
67
|
+
function stripSuffix(ref: string): string {
|
|
68
|
+
return ref.replace(/[?#].*$/, "");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function inlineLocalAssets(html: string, options: InlineOptions): InlineReport {
|
|
72
|
+
const { baseDir, maxAssetBytes, maxTotalBytes } = options;
|
|
73
|
+
|
|
74
|
+
const inlined: InlineReport["inlined"] = [];
|
|
75
|
+
const unresolved: string[] = [];
|
|
76
|
+
const skipped: InlineReport["skipped"] = [];
|
|
77
|
+
|
|
78
|
+
// Same file referenced twice should only be read (and counted) once.
|
|
79
|
+
const cache = new Map<string, string | null>();
|
|
80
|
+
let budget = maxTotalBytes - Buffer.byteLength(html, "utf-8");
|
|
81
|
+
|
|
82
|
+
/** Returns the data URI for a reference, or null to leave it untouched. */
|
|
83
|
+
function toDataUri(rawRef: string): string | null {
|
|
84
|
+
const ref = rawRef.trim();
|
|
85
|
+
if (!ref || isNonLocal(ref)) return null;
|
|
86
|
+
|
|
87
|
+
const cached = cache.get(ref);
|
|
88
|
+
if (cached !== undefined) return cached;
|
|
89
|
+
|
|
90
|
+
const relPath = stripSuffix(ref);
|
|
91
|
+
if (!relPath) return null;
|
|
92
|
+
|
|
93
|
+
const abs = isAbsolute(relPath) ? relPath : resolve(baseDir, relPath);
|
|
94
|
+
|
|
95
|
+
let bytes: number;
|
|
96
|
+
try {
|
|
97
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) {
|
|
98
|
+
unresolved.push(ref);
|
|
99
|
+
cache.set(ref, null);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
bytes = statSync(abs).size;
|
|
103
|
+
} catch {
|
|
104
|
+
unresolved.push(ref);
|
|
105
|
+
cache.set(ref, null);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (bytes > maxAssetBytes) {
|
|
110
|
+
skipped.push({ ref, bytes, reason: "asset-too-large" });
|
|
111
|
+
cache.set(ref, null);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// base64 costs ~4/3 of the raw bytes; check against the remaining budget
|
|
116
|
+
// before reading so an oversized asset never lands in memory twice.
|
|
117
|
+
const encodedCost = Math.ceil(bytes / 3) * 4;
|
|
118
|
+
if (encodedCost > budget) {
|
|
119
|
+
skipped.push({ ref, bytes, reason: "document-too-large" });
|
|
120
|
+
cache.set(ref, null);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let uri: string;
|
|
125
|
+
try {
|
|
126
|
+
const mime = MIME_BY_EXT[extname(abs).toLowerCase()] ?? "application/octet-stream";
|
|
127
|
+
uri = `data:${mime};base64,${readFileSync(abs).toString("base64")}`;
|
|
128
|
+
} catch {
|
|
129
|
+
unresolved.push(ref);
|
|
130
|
+
cache.set(ref, null);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
budget -= uri.length;
|
|
135
|
+
inlined.push({ ref, bytes });
|
|
136
|
+
cache.set(ref, uri);
|
|
137
|
+
return uri;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** `a.png 1x, b.png 2x` — rewrite each candidate, keep its descriptor. */
|
|
141
|
+
function rewriteSrcset(value: string): string {
|
|
142
|
+
return value
|
|
143
|
+
.split(",")
|
|
144
|
+
.map((candidate) => {
|
|
145
|
+
const trimmed = candidate.trim();
|
|
146
|
+
if (!trimmed) return candidate;
|
|
147
|
+
const [url, ...descriptor] = trimmed.split(/\s+/);
|
|
148
|
+
const uri = url ? toDataUri(url) : null;
|
|
149
|
+
if (!uri) return candidate;
|
|
150
|
+
return [uri, ...descriptor].join(" ");
|
|
151
|
+
})
|
|
152
|
+
.join(", ");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let out = html;
|
|
156
|
+
|
|
157
|
+
// src="..." on img/script/source/video/audio/iframe, and poster="..."
|
|
158
|
+
out = out.replace(
|
|
159
|
+
/(\b(?:src|poster)\s*=\s*)(["'])([^"']*)\2/gi,
|
|
160
|
+
(match, prefix: string, quote: string, ref: string) => {
|
|
161
|
+
const uri = toDataUri(ref);
|
|
162
|
+
return uri ? `${prefix}${quote}${uri}${quote}` : match;
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// srcset="a.png 1x, b.png 2x"
|
|
167
|
+
out = out.replace(
|
|
168
|
+
/(\bsrcset\s*=\s*)(["'])([^"']*)\2/gi,
|
|
169
|
+
(match, prefix: string, quote: string, value: string) => {
|
|
170
|
+
const rewritten = rewriteSrcset(value);
|
|
171
|
+
return rewritten === value ? match : `${prefix}${quote}${rewritten}${quote}`;
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// <link href="style.css"> / icons — but never <a href>.
|
|
176
|
+
out = out.replace(/<link\b[^>]*>/gi, (tag) =>
|
|
177
|
+
tag.replace(
|
|
178
|
+
/(\bhref\s*=\s*)(["'])([^"']*)\2/i,
|
|
179
|
+
(match, prefix: string, quote: string, ref: string) => {
|
|
180
|
+
const uri = toDataUri(ref);
|
|
181
|
+
return uri ? `${prefix}${quote}${uri}${quote}` : match;
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// CSS url(...) in <style> blocks and style="" attributes.
|
|
187
|
+
out = out.replace(/url\(\s*(["']?)([^"')]+)\1\s*\)/gi, (match, quote: string, ref: string) => {
|
|
188
|
+
const uri = toDataUri(ref);
|
|
189
|
+
return uri ? `url(${quote}${uri}${quote})` : match;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { html: out, inlined, unresolved, skipped };
|
|
193
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `publish_html` — hand the user a public link to a rendered HTML document.
|
|
3
|
+
*
|
|
4
|
+
* This sits beside `show_html`: both exist to show rendered HTML rather than
|
|
5
|
+
* raw markup, so both are gated by the same `visualTools` capability. They
|
|
6
|
+
* differ only in delivery. `show_html` paints a sandboxed in-app panel, which
|
|
7
|
+
* blocks network requests and cannot be opened outside the app; publishing
|
|
8
|
+
* returns an ordinary URL that survives the turn and can be shared.
|
|
9
|
+
*
|
|
10
|
+
* Requires a snippet backend (`NEGOTIUM_SNIPPETS_API_URL`). The tool is left
|
|
11
|
+
* out of the catalog entirely when none is configured, so a deployment
|
|
12
|
+
* without one never advertises a link it cannot mint.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { errorResult, type SharedMcpTool, textResult } from "#agents/mcp-tools/common";
|
|
16
|
+
import { inlineLocalAssets } from "#agents/mcp-tools/inline-assets";
|
|
17
|
+
import { SNIPPETS_API_URL } from "#platform/config";
|
|
18
|
+
|
|
19
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
20
|
+
/** Keep in step with the snippet service's own body limit. */
|
|
21
|
+
export const MAX_PUBLISHED_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
export const MAX_PUBLISHED_ASSET_BYTES = 5 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
export interface PublishHtmlContext {
|
|
25
|
+
/** Directory that relative asset paths resolve against (the agent's cwd). */
|
|
26
|
+
cwd?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type SnippetResponse = {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
url?: string;
|
|
32
|
+
snippetId?: string;
|
|
33
|
+
expiresAt?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function formatPublishBytes(bytes: number): string {
|
|
38
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
39
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`;
|
|
40
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Accept a full snippet URL or a bare id, return the id. */
|
|
44
|
+
export function parseSnippetId(input: string): string | null {
|
|
45
|
+
const trimmed = input.trim();
|
|
46
|
+
const fromUrl = trimmed.match(/\/snippets\/([A-Za-z0-9-]+)/)?.[1];
|
|
47
|
+
const id = fromUrl ?? trimmed;
|
|
48
|
+
return /^[A-Za-z0-9-]+$/.test(id) ? id : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function snippetRequest(
|
|
52
|
+
baseUrl: string,
|
|
53
|
+
path: string,
|
|
54
|
+
init: RequestInit = {},
|
|
55
|
+
): Promise<SnippetResponse> {
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
58
|
+
...init,
|
|
59
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
60
|
+
});
|
|
61
|
+
const body = (await response.json().catch(() => null)) as SnippetResponse | null;
|
|
62
|
+
if (body) return body;
|
|
63
|
+
return { ok: false, error: `snippet service returned ${response.status}` };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : "snippet service unreachable";
|
|
66
|
+
return { ok: false, error: message };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build the publish tools, or `[]` when no snippet backend is configured.
|
|
72
|
+
* `ctx` may be a getter so a long-lived server picks up the current turn's cwd.
|
|
73
|
+
*/
|
|
74
|
+
export function createPublishHtmlToolDefinitions(
|
|
75
|
+
ctx: PublishHtmlContext | (() => PublishHtmlContext),
|
|
76
|
+
options: { apiUrl?: string } = {},
|
|
77
|
+
): SharedMcpTool[] {
|
|
78
|
+
const apiUrl = (options.apiUrl ?? SNIPPETS_API_URL)?.replace(/\/+$/, "");
|
|
79
|
+
if (!apiUrl) return [];
|
|
80
|
+
const getCtx = typeof ctx === "function" ? ctx : () => ctx;
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
name: "publish_html",
|
|
85
|
+
description: [
|
|
86
|
+
"Publish an HTML document and return a public, shareable https link that",
|
|
87
|
+
"stays live for 72 hours. Use this when the user should be able to open,",
|
|
88
|
+
"keep, or send the result — a report, dashboard, or styled table — rather",
|
|
89
|
+
"than only glance at it in the side panel.",
|
|
90
|
+
"Pass a complete standalone document and inline the CSS and JS.",
|
|
91
|
+
"For charts and diagrams prefer inline <svg>: it stays sharp at any zoom",
|
|
92
|
+
"and is far smaller than an image file.",
|
|
93
|
+
"Images may be remote URLs, data: URIs, or paths to local files you",
|
|
94
|
+
"created — local files are embedded automatically, so the page keeps",
|
|
95
|
+
"working after your workspace is gone.",
|
|
96
|
+
].join(" "),
|
|
97
|
+
schema: {
|
|
98
|
+
html: z
|
|
99
|
+
.string()
|
|
100
|
+
.describe("Complete standalone HTML document to publish. Inline the CSS and JS."),
|
|
101
|
+
base_dir: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe(
|
|
105
|
+
"Directory that relative image paths resolve against. Defaults to the working directory.",
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
async handler(input: { html?: string; base_dir?: string }) {
|
|
109
|
+
const html = input?.html ?? "";
|
|
110
|
+
if (!html.trim()) return errorResult("html is empty — provide an HTML document.");
|
|
111
|
+
|
|
112
|
+
const baseDir = input.base_dir?.trim() || getCtx().cwd || process.cwd();
|
|
113
|
+
const report = inlineLocalAssets(html, {
|
|
114
|
+
baseDir,
|
|
115
|
+
maxAssetBytes: MAX_PUBLISHED_ASSET_BYTES,
|
|
116
|
+
maxTotalBytes: MAX_PUBLISHED_BYTES,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const bytes = Buffer.byteLength(report.html, "utf-8");
|
|
120
|
+
if (bytes > MAX_PUBLISHED_BYTES) {
|
|
121
|
+
return errorResult(
|
|
122
|
+
`Document is ${formatPublishBytes(bytes)} after embedding images, over the ${formatPublishBytes(MAX_PUBLISHED_BYTES)} limit. Use fewer or smaller images, or switch charts to inline <svg>.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const result = await snippetRequest(apiUrl, "/snippets", {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
129
|
+
body: report.html,
|
|
130
|
+
});
|
|
131
|
+
if (!result.ok || !result.url) {
|
|
132
|
+
return errorResult(result.error ?? "Failed to publish.");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Surface asset problems here so a broken image is visible now rather
|
|
136
|
+
// than only in the user's browser.
|
|
137
|
+
const notes: string[] = [];
|
|
138
|
+
if (report.inlined.length > 0) {
|
|
139
|
+
const total = report.inlined.reduce((sum, a) => sum + a.bytes, 0);
|
|
140
|
+
notes.push(
|
|
141
|
+
`Embedded ${report.inlined.length} local file(s), ${formatPublishBytes(total)}.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (report.unresolved.length > 0) {
|
|
145
|
+
notes.push(
|
|
146
|
+
`WARNING — these look like local files but were not found, so they will render broken: ${report.unresolved.join(", ")}. Check the paths (relative to ${baseDir}) and republish.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
for (const s of report.skipped) {
|
|
150
|
+
notes.push(
|
|
151
|
+
s.reason === "asset-too-large"
|
|
152
|
+
? `WARNING — ${s.ref} is ${formatPublishBytes(s.bytes)}, over the ${formatPublishBytes(MAX_PUBLISHED_ASSET_BYTES)} per-file limit, and was left as a broken link.`
|
|
153
|
+
: `WARNING — ${s.ref} was left out because the document hit the ${formatPublishBytes(MAX_PUBLISHED_BYTES)} limit.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const payload = {
|
|
158
|
+
url: result.url,
|
|
159
|
+
snippetId: result.snippetId,
|
|
160
|
+
expiresAt: result.expiresAt,
|
|
161
|
+
sizeBytes: bytes,
|
|
162
|
+
...(report.inlined.length > 0 ? { embeddedFiles: report.inlined.length } : {}),
|
|
163
|
+
};
|
|
164
|
+
return textResult([JSON.stringify(payload, null, 2), ...notes].join("\n\n"));
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "unpublish_html",
|
|
169
|
+
description:
|
|
170
|
+
"Delete a previously published HTML document before its 72-hour expiry. Accepts the URL returned by publish_html, or its id.",
|
|
171
|
+
schema: {
|
|
172
|
+
snippet: z.string().describe("URL returned by publish_html, or the bare id."),
|
|
173
|
+
},
|
|
174
|
+
async handler(input: { snippet?: string }) {
|
|
175
|
+
const id = parseSnippetId(input?.snippet ?? "");
|
|
176
|
+
if (!id) return errorResult("Could not read a published document id from that value.");
|
|
177
|
+
const result = await snippetRequest(apiUrl, `/snippets/${id}`, { method: "DELETE" });
|
|
178
|
+
if (!result.ok) return errorResult(result.error ?? "Failed to delete.");
|
|
179
|
+
return textResult(`Published document ${id} deleted.`);
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Stable core surface used by the embedded runtime MCP endpoint. */
|
|
2
2
|
export { createAskUserToolDefinition } from "#agents/mcp-tools/ask-user";
|
|
3
3
|
export { errorResult, textResult } from "#agents/mcp-tools/common";
|
|
4
|
+
export { createPublishHtmlToolDefinitions } from "#agents/mcp-tools/publish-html";
|
|
4
5
|
export type { SelfConfigContext } from "#agents/mcp-tools/self-config";
|
|
5
6
|
export { createSelfConfigToolDefinitions } from "#agents/mcp-tools/self-config";
|
|
6
7
|
export {
|
|
@@ -153,6 +153,18 @@ export function resolveBrowserRsBin(envValue?: string): string | undefined {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Backend that stores documents published by `publish_html` and serves them
|
|
158
|
+
* at `<base>/snippets/<id>`. Unset means the deployment has nowhere to
|
|
159
|
+
* publish, and the publish tools are omitted from the catalog rather than
|
|
160
|
+
* offering a link that cannot be minted.
|
|
161
|
+
*/
|
|
162
|
+
export const SNIPPETS_API_URL = (
|
|
163
|
+
envText("NEGOTIUM_SNIPPETS_API_URL") ??
|
|
164
|
+
envText("SNIPPETS_API_URL") ??
|
|
165
|
+
""
|
|
166
|
+
).replace(/\/+$/, "");
|
|
167
|
+
|
|
156
168
|
export const BROWSER_RS_BIN = resolveBrowserRsBin(envText("NEGOTIUM_BROWSER_RS_BIN"));
|
|
157
169
|
|
|
158
170
|
// Managed Browser.rs terminates MCP transports and security policy itself.
|
|
@@ -1423,9 +1423,29 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1423
1423
|
].join("\n");
|
|
1424
1424
|
}
|
|
1425
1425
|
|
|
1426
|
+
/**
|
|
1427
|
+
* Per-turn `<system-reminder>` blocks, appended to the *user* prompt rather
|
|
1428
|
+
* than the system prompt.
|
|
1429
|
+
*
|
|
1430
|
+
* These facts flip between turns on the same model — whether the browser MCP
|
|
1431
|
+
* came up, whether this turn is an auto-resume. A prompt cache invalidates
|
|
1432
|
+
* from the first diverging byte, so putting them in the system prompt means
|
|
1433
|
+
* every flip re-reads the entire prompt: measured against the live DeepSeek
|
|
1434
|
+
* API, one flip at turn 15 of a 20-turn conversation cost 17,852 uncached
|
|
1435
|
+
* tokens (hit=0) versus 1,215 with the reminder on the user turn — 40% more
|
|
1436
|
+
* uncached tokens across the run. On the user turn the divergence lands at
|
|
1437
|
+
* the tail, where the text is new anyway.
|
|
1438
|
+
*
|
|
1439
|
+
* Applied after the conversation log is written, so a reminder never becomes
|
|
1440
|
+
* part of the recorded user message (it would then persist into synthesized
|
|
1441
|
+
* rollouts and defeat the point).
|
|
1442
|
+
*/
|
|
1443
|
+
const turnReminders: string[] = [];
|
|
1444
|
+
|
|
1426
1445
|
if (params.from === FROM_AUTO_CONTINUE) {
|
|
1427
|
-
|
|
1428
|
-
"
|
|
1446
|
+
turnReminders.push(
|
|
1447
|
+
"<system-reminder>이 턴은 설정 자동 조정(effort/model/agent) 후 자동 재개된 턴이다. effort/model/agent 난이도 재평가 및 설정 변경 없이 즉시 작업을 시작할 것.</system-reminder>",
|
|
1448
|
+
);
|
|
1429
1449
|
}
|
|
1430
1450
|
|
|
1431
1451
|
if (!silent && !sessionRetried) {
|
|
@@ -1538,25 +1558,28 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1538
1558
|
);
|
|
1539
1559
|
}
|
|
1540
1560
|
}
|
|
1541
|
-
let effectiveSystemPrompt = systemPrompt;
|
|
1542
1561
|
if (playwrightRequested && !browserProfileOwner) {
|
|
1543
|
-
|
|
1544
|
-
"<system-reminder>Browser tools are unavailable in this turn because browser profiles are private to the topic owner. Do not attempt browser calls or ask to reuse the owner's login state.</system-reminder>"
|
|
1545
|
-
|
|
1562
|
+
turnReminders.push(
|
|
1563
|
+
"<system-reminder>Browser tools are unavailable in this turn because browser profiles are private to the topic owner. Do not attempt browser calls or ask to reuse the owner's login state.</system-reminder>",
|
|
1564
|
+
);
|
|
1546
1565
|
}
|
|
1547
1566
|
if (consumePlaywrightUnavailable(userId, topic.title)) {
|
|
1548
|
-
|
|
1549
|
-
"<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>"
|
|
1550
|
-
|
|
1567
|
+
turnReminders.push(
|
|
1568
|
+
"<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>",
|
|
1569
|
+
);
|
|
1551
1570
|
}
|
|
1571
|
+
// The system prompt is now byte-identical across turns of a topic, so the
|
|
1572
|
+
// provider's prefix cache survives; per-turn facts ride the prompt tail.
|
|
1573
|
+
const effectivePrompt =
|
|
1574
|
+
turnReminders.length > 0 ? `${agentPrompt}\n\n${turnReminders.join("\n\n")}` : agentPrompt;
|
|
1552
1575
|
|
|
1553
1576
|
try {
|
|
1554
1577
|
yield* runAgent({
|
|
1555
1578
|
agent: agentKind,
|
|
1556
|
-
prompt:
|
|
1579
|
+
prompt: effectivePrompt,
|
|
1557
1580
|
attachments: promptAttachments,
|
|
1558
1581
|
cwd: workspaceCwd,
|
|
1559
|
-
systemPrompt
|
|
1582
|
+
systemPrompt,
|
|
1560
1583
|
sessionId,
|
|
1561
1584
|
userId,
|
|
1562
1585
|
vaultUserId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const NEGOTIUM_VERSION = "0.2.
|
|
1
|
+
export const NEGOTIUM_VERSION = "0.2.10";
|
package/dist/runtime-helpers.js
CHANGED
|
@@ -494,6 +494,7 @@ function resolveBrowserRsBin(envValue) {
|
|
|
494
494
|
return;
|
|
495
495
|
}
|
|
496
496
|
}
|
|
497
|
+
var SNIPPETS_API_URL = (envText("NEGOTIUM_SNIPPETS_API_URL") ?? envText("SNIPPETS_API_URL") ?? "").replace(/\/+$/, "");
|
|
497
498
|
var BROWSER_RS_BIN = resolveBrowserRsBin(envText("NEGOTIUM_BROWSER_RS_BIN"));
|
|
498
499
|
function resolveBrowserMcpBin(envValue) {
|
|
499
500
|
const override = envValue?.trim();
|
|
@@ -1132,4 +1133,4 @@ export {
|
|
|
1132
1133
|
CLAUDE_EFFORT_VALUES
|
|
1133
1134
|
};
|
|
1134
1135
|
|
|
1135
|
-
//# debugId=
|
|
1136
|
+
//# debugId=842C00B02967857064756E2164756E21
|