@poncho-ai/cli 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +167 -0
- package/dist/{chunk-U643TWFX.js → chunk-XCDN62XL.js} +1983 -135
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-CE7U47S5.js → run-interactive-ink-W5YJS7UH.js} +1 -1
- package/package.json +4 -4
- package/src/cron-helpers.ts +13 -4
- package/src/index.ts +441 -21
- package/src/vfs-zip.ts +94 -0
- package/src/web-ui-client.ts +1028 -26
- package/src/web-ui-styles.ts +413 -15
- package/src/web-ui.ts +6 -1
package/src/index.ts
CHANGED
|
@@ -55,6 +55,7 @@ import type {
|
|
|
55
55
|
ApiThreadSummary,
|
|
56
56
|
} from "@poncho-ai/sdk";
|
|
57
57
|
import { getTextContent } from "@poncho-ai/sdk";
|
|
58
|
+
import { buildZip, type ZipEntry } from "./vfs-zip.js";
|
|
58
59
|
import {
|
|
59
60
|
AgentBridge,
|
|
60
61
|
ResendAdapter,
|
|
@@ -134,6 +135,22 @@ const collectToolCallIds = (msgs: Message[]): Set<string> => {
|
|
|
134
135
|
}
|
|
135
136
|
return ids;
|
|
136
137
|
};
|
|
138
|
+
|
|
139
|
+
const MEMORY_VFILE_PATH = "/memory.md";
|
|
140
|
+
|
|
141
|
+
const isSafeVfsPath = (p: string): boolean => {
|
|
142
|
+
if (typeof p !== "string" || p.length === 0) return false;
|
|
143
|
+
if (!p.startsWith("/")) return false;
|
|
144
|
+
if (p.includes("\0")) return false;
|
|
145
|
+
const segments = p.split("/").slice(1);
|
|
146
|
+
if (p !== "/" && segments[segments.length - 1] === "") return false;
|
|
147
|
+
for (const seg of segments) {
|
|
148
|
+
if (seg === "" && p !== "/") return false;
|
|
149
|
+
if (seg === "." || seg === "..") return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
};
|
|
153
|
+
|
|
137
154
|
const serverlessLog = createLogger("serverless");
|
|
138
155
|
import {
|
|
139
156
|
type DeployTarget,
|
|
@@ -307,6 +324,13 @@ export type RequestHandler = ((
|
|
|
307
324
|
_finishConversationStream?: (conversationId: string) => void;
|
|
308
325
|
_checkAndFireReminders?: () => Promise<{ fired: string[]; count: number; duration: number }>;
|
|
309
326
|
_reminderPollIntervalMs?: number;
|
|
327
|
+
_buildTurnParameters?: (
|
|
328
|
+
conversation: Conversation,
|
|
329
|
+
opts?: {
|
|
330
|
+
bodyParameters?: Record<string, unknown>;
|
|
331
|
+
messagingMetadata?: { platform: string; sender: { id: string; name?: string | null }; threadId?: string };
|
|
332
|
+
},
|
|
333
|
+
) => Record<string, unknown>;
|
|
310
334
|
};
|
|
311
335
|
|
|
312
336
|
export const createRequestHandler = async (options?: {
|
|
@@ -355,6 +379,32 @@ export const createRequestHandler = async (options?: {
|
|
|
355
379
|
const conversationEventStreams = new Map<string, ConversationEventStream>();
|
|
356
380
|
type EventCallback = (event: AgentEvent) => void;
|
|
357
381
|
const conversationEventCallbacks = new Map<string, Set<EventCallback>>();
|
|
382
|
+
// Per-conversation replay-buffer cap. Live subscribers get full events; the
|
|
383
|
+
// buffer is just so a reconnecting client can catch up. Keep the most recent
|
|
384
|
+
// N events to bound memory.
|
|
385
|
+
const MAX_BUFFERED_EVENTS_PER_CONVERSATION = 1000;
|
|
386
|
+
// Deep-clone an event with any string > 4 KB replaced by a placeholder. Used
|
|
387
|
+
// when buffering for replay: a reconnecting client doesn't need fresh
|
|
388
|
+
// screenshots/large blobs (they're persisted in the conversation), and
|
|
389
|
+
// accumulating them caused OOMs (e.g. tool:completed for browser_screenshot
|
|
390
|
+
// carries a ~134 KB base64 JPEG per call).
|
|
391
|
+
const STRIP_LARGE_STRING_BYTES = 4096;
|
|
392
|
+
const stripLargeStringsForBuffer = (value: unknown): unknown => {
|
|
393
|
+
if (typeof value === "string") {
|
|
394
|
+
return value.length > STRIP_LARGE_STRING_BYTES
|
|
395
|
+
? `[stripped-for-replay len=${value.length}]`
|
|
396
|
+
: value;
|
|
397
|
+
}
|
|
398
|
+
if (Array.isArray(value)) return value.map(stripLargeStringsForBuffer);
|
|
399
|
+
if (value && typeof value === "object") {
|
|
400
|
+
const out: Record<string, unknown> = {};
|
|
401
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
402
|
+
out[k] = stripLargeStringsForBuffer(v);
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
return value;
|
|
407
|
+
};
|
|
358
408
|
const broadcastEvent = (conversationId: string, event: AgentEvent): void => {
|
|
359
409
|
let stream = conversationEventStreams.get(conversationId);
|
|
360
410
|
if (!stream) {
|
|
@@ -365,7 +415,10 @@ export const createRequestHandler = async (options?: {
|
|
|
365
415
|
// Buffering them for reconnect replay grew to multi-GB and OOM'd the process;
|
|
366
416
|
// they're ephemeral like browser:status and should never replay.
|
|
367
417
|
if (event.type !== "browser:frame") {
|
|
368
|
-
stream.buffer.push(event);
|
|
418
|
+
stream.buffer.push(stripLargeStringsForBuffer(event) as AgentEvent);
|
|
419
|
+
if (stream.buffer.length > MAX_BUFFERED_EVENTS_PER_CONVERSATION) {
|
|
420
|
+
stream.buffer.splice(0, stream.buffer.length - MAX_BUFFERED_EVENTS_PER_CONVERSATION);
|
|
421
|
+
}
|
|
369
422
|
}
|
|
370
423
|
for (const subscriber of stream.subscribers) {
|
|
371
424
|
try {
|
|
@@ -681,6 +734,40 @@ export const createRequestHandler = async (options?: {
|
|
|
681
734
|
};
|
|
682
735
|
};
|
|
683
736
|
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
// Single helper for assembling runInput.parameters across every turn entry
|
|
739
|
+
// point (HTTP route, messaging adapter, cron, reminder). All `__`-prefixed
|
|
740
|
+
// context params live here so adding a new one only requires one edit.
|
|
741
|
+
// ---------------------------------------------------------------------------
|
|
742
|
+
const buildTurnParameters = (
|
|
743
|
+
conversation: Conversation,
|
|
744
|
+
opts: {
|
|
745
|
+
bodyParameters?: Record<string, unknown>;
|
|
746
|
+
messagingMetadata?: {
|
|
747
|
+
platform: string;
|
|
748
|
+
sender: { id: string; name?: string | null };
|
|
749
|
+
threadId?: string;
|
|
750
|
+
};
|
|
751
|
+
} = {},
|
|
752
|
+
): Record<string, unknown> => {
|
|
753
|
+
return withToolResultArchiveParam({
|
|
754
|
+
...(opts.bodyParameters ?? {}),
|
|
755
|
+
...buildRecallParams({
|
|
756
|
+
ownerId: conversation.ownerId,
|
|
757
|
+
tenantId: conversation.tenantId,
|
|
758
|
+
excludeConversationId: conversation.conversationId,
|
|
759
|
+
}),
|
|
760
|
+
...(opts.messagingMetadata ? {
|
|
761
|
+
__messaging_platform: opts.messagingMetadata.platform,
|
|
762
|
+
__messaging_sender_id: opts.messagingMetadata.sender.id,
|
|
763
|
+
__messaging_sender_name: opts.messagingMetadata.sender.name ?? "",
|
|
764
|
+
__messaging_thread_id: opts.messagingMetadata.threadId,
|
|
765
|
+
} : {}),
|
|
766
|
+
__activeConversationId: conversation.conversationId,
|
|
767
|
+
__ownerId: conversation.ownerId,
|
|
768
|
+
}, conversation);
|
|
769
|
+
};
|
|
770
|
+
|
|
684
771
|
// Subagent lifecycle extracted to AgentOrchestrator (Phase 5).
|
|
685
772
|
|
|
686
773
|
// ---------------------------------------------------------------------------
|
|
@@ -790,6 +877,7 @@ export const createRequestHandler = async (options?: {
|
|
|
790
877
|
let runContextWindow = 0;
|
|
791
878
|
let runContinuation = false;
|
|
792
879
|
let runContinuationMessages: Message[] | undefined;
|
|
880
|
+
let runHarnessMessages: Message[] | undefined;
|
|
793
881
|
let runSteps = 0;
|
|
794
882
|
let runMaxSteps: number | undefined;
|
|
795
883
|
|
|
@@ -828,17 +916,22 @@ export const createRequestHandler = async (options?: {
|
|
|
828
916
|
const runInput = {
|
|
829
917
|
task: input.task,
|
|
830
918
|
conversationId,
|
|
919
|
+
tenantId: latestConversation?.tenantId ?? undefined,
|
|
831
920
|
messages: historyMessages,
|
|
832
921
|
files: input.files,
|
|
833
|
-
parameters:
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
922
|
+
parameters: latestConversation
|
|
923
|
+
? buildTurnParameters(latestConversation, {
|
|
924
|
+
messagingMetadata: input.metadata,
|
|
925
|
+
})
|
|
926
|
+
: withToolResultArchiveParam({
|
|
927
|
+
...(input.metadata ? {
|
|
928
|
+
__messaging_platform: input.metadata.platform,
|
|
929
|
+
__messaging_sender_id: input.metadata.sender.id,
|
|
930
|
+
__messaging_sender_name: input.metadata.sender.name ?? "",
|
|
931
|
+
__messaging_thread_id: input.metadata.threadId,
|
|
932
|
+
} : {}),
|
|
933
|
+
__activeConversationId: conversationId,
|
|
934
|
+
}, { _toolResultArchive: {} } as Conversation),
|
|
842
935
|
};
|
|
843
936
|
|
|
844
937
|
try {
|
|
@@ -939,6 +1032,7 @@ export const createRequestHandler = async (options?: {
|
|
|
939
1032
|
});
|
|
940
1033
|
runContinuation = execution.runContinuation;
|
|
941
1034
|
runContinuationMessages = execution.runContinuationMessages;
|
|
1035
|
+
runHarnessMessages = execution.runHarnessMessages;
|
|
942
1036
|
runSteps = execution.runSteps;
|
|
943
1037
|
runMaxSteps = execution.runMaxSteps;
|
|
944
1038
|
runContextTokens = execution.runContextTokens;
|
|
@@ -962,7 +1056,11 @@ export const createRequestHandler = async (options?: {
|
|
|
962
1056
|
contextWindow: runContextWindow,
|
|
963
1057
|
continuation: runContinuation,
|
|
964
1058
|
continuationMessages: runContinuationMessages,
|
|
965
|
-
|
|
1059
|
+
// Prefer the cancellation/end-of-run snapshot from the harness so
|
|
1060
|
+
// _harnessMessages stays in sync with what the model just saw,
|
|
1061
|
+
// even on aborted runs. Falls back to continuationMessages when
|
|
1062
|
+
// the run completed via continuation.
|
|
1063
|
+
harnessMessages: runHarnessMessages ?? runContinuationMessages,
|
|
966
1064
|
toolResultArchive: harness.getToolResultArchive(conversationId),
|
|
967
1065
|
}, { shouldRebuildCanonical: true });
|
|
968
1066
|
});
|
|
@@ -1428,7 +1526,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1428
1526
|
}
|
|
1429
1527
|
|
|
1430
1528
|
if (webUiEnabled) {
|
|
1431
|
-
if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
|
|
1529
|
+
if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/") || pathname.startsWith("/f/"))) {
|
|
1432
1530
|
writeHtml(response, 200, renderWebUiHtml({ agentName, isDev: !isProduction }));
|
|
1433
1531
|
return;
|
|
1434
1532
|
}
|
|
@@ -2650,13 +2748,16 @@ export const createRequestHandler = async (options?: {
|
|
|
2650
2748
|
&& Array.isArray(conversation._continuationMessages)
|
|
2651
2749
|
&& conversation._continuationMessages.length > 0
|
|
2652
2750
|
&& !hasPendingApprovals;
|
|
2751
|
+
const verboseDev = process.env.PONCHO_DEV_VERBOSE === "1";
|
|
2653
2752
|
writeJson(response, 200, {
|
|
2654
2753
|
conversation: {
|
|
2655
2754
|
...conversation,
|
|
2656
2755
|
messages: conversation.messages.map(normalizeMessageForClient).filter((m): m is Message => m !== null),
|
|
2657
2756
|
pendingApprovals: storedPending,
|
|
2658
2757
|
_continuationMessages: undefined,
|
|
2659
|
-
|
|
2758
|
+
// In verbose dev mode the web UI exposes a toggle to inspect the
|
|
2759
|
+
// raw harness messages sent to the model API. Strip it otherwise.
|
|
2760
|
+
_harnessMessages: verboseDev ? conversation._harnessMessages : undefined,
|
|
2660
2761
|
// The browser has no use for the archive; make sure we never ship
|
|
2661
2762
|
// it back even if the conversation was loaded via getWithArchive.
|
|
2662
2763
|
_toolResultArchive: undefined,
|
|
@@ -2665,6 +2766,7 @@ export const createRequestHandler = async (options?: {
|
|
|
2665
2766
|
hasActiveRun: hasActiveRun || hasPendingCallbackResults,
|
|
2666
2767
|
hasRunningSubagents,
|
|
2667
2768
|
needsContinuation,
|
|
2769
|
+
verboseDev,
|
|
2668
2770
|
});
|
|
2669
2771
|
return;
|
|
2670
2772
|
}
|
|
@@ -2789,6 +2891,22 @@ export const createRequestHandler = async (options?: {
|
|
|
2789
2891
|
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
2790
2892
|
return;
|
|
2791
2893
|
}
|
|
2894
|
+
if (vfsPath === MEMORY_VFILE_PATH) {
|
|
2895
|
+
try {
|
|
2896
|
+
const memory = await engine.memory.get(tenantId);
|
|
2897
|
+
const data = Buffer.from(memory.content, "utf-8");
|
|
2898
|
+
response.writeHead(200, {
|
|
2899
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
2900
|
+
"Content-Length": data.length,
|
|
2901
|
+
"Content-Disposition": `inline; filename="memory.md"`,
|
|
2902
|
+
"Cache-Control": "no-cache",
|
|
2903
|
+
});
|
|
2904
|
+
response.end(data);
|
|
2905
|
+
} catch (err) {
|
|
2906
|
+
writeJson(response, 500, { code: "READ_FAILED", message: (err as Error)?.message ?? "Failed to read memory" });
|
|
2907
|
+
}
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2792
2910
|
try {
|
|
2793
2911
|
const stat = await engine.vfs.stat(tenantId, vfsPath);
|
|
2794
2912
|
if (!stat || stat.type !== "file") {
|
|
@@ -2823,6 +2941,273 @@ export const createRequestHandler = async (options?: {
|
|
|
2823
2941
|
return;
|
|
2824
2942
|
}
|
|
2825
2943
|
|
|
2944
|
+
if (vfsMatch && request.method === "PUT") {
|
|
2945
|
+
const rawPath = "/" + decodeURIComponent(vfsMatch[1] ?? "");
|
|
2946
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
2947
|
+
const engine = harness.storageEngine;
|
|
2948
|
+
if (!engine) {
|
|
2949
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (!isSafeVfsPath(rawPath) || rawPath === "/") {
|
|
2953
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
if (rawPath === MEMORY_VFILE_PATH) {
|
|
2957
|
+
try {
|
|
2958
|
+
const chunks: Buffer[] = [];
|
|
2959
|
+
for await (const chunk of request) chunks.push(chunk as Buffer);
|
|
2960
|
+
const body = Buffer.concat(chunks);
|
|
2961
|
+
const content = body.toString("utf-8").trim();
|
|
2962
|
+
const memory = await engine.memory.update(content, tenantId);
|
|
2963
|
+
writeJson(response, 200, {
|
|
2964
|
+
path: MEMORY_VFILE_PATH,
|
|
2965
|
+
size: Buffer.byteLength(memory.content, "utf-8"),
|
|
2966
|
+
mimeType: "text/markdown",
|
|
2967
|
+
updatedAt: memory.updatedAt,
|
|
2968
|
+
});
|
|
2969
|
+
} catch (err) {
|
|
2970
|
+
writeJson(response, 500, { code: "WRITE_FAILED", message: (err as Error)?.message ?? "Failed to write memory" });
|
|
2971
|
+
}
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
const allowOverwrite = requestUrl.searchParams.get("overwrite") === "1";
|
|
2975
|
+
try {
|
|
2976
|
+
const existing = await engine.vfs.stat(tenantId, rawPath);
|
|
2977
|
+
if (existing && !allowOverwrite) {
|
|
2978
|
+
writeJson(response, 409, { code: "EXISTS", message: "File already exists" });
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
if (existing && existing.type !== "file") {
|
|
2982
|
+
writeJson(response, 409, { code: "NOT_A_FILE", message: "Path exists and is not a file" });
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
const chunks: Buffer[] = [];
|
|
2986
|
+
for await (const chunk of request) chunks.push(chunk as Buffer);
|
|
2987
|
+
const body = Buffer.concat(chunks);
|
|
2988
|
+
const mimeType = (request.headers["content-type"] as string | undefined)?.split(";")[0]?.trim() || undefined;
|
|
2989
|
+
await engine.vfs.writeFile(tenantId, rawPath, new Uint8Array(body), mimeType);
|
|
2990
|
+
if (rawPath === "/skills" || rawPath.startsWith("/skills/")) {
|
|
2991
|
+
harness.invalidateSkillsForTenant(tenantId);
|
|
2992
|
+
}
|
|
2993
|
+
const stat = await engine.vfs.stat(tenantId, rawPath);
|
|
2994
|
+
writeJson(response, 200, {
|
|
2995
|
+
path: rawPath,
|
|
2996
|
+
size: stat?.size ?? body.length,
|
|
2997
|
+
mimeType: stat?.mimeType ?? mimeType ?? null,
|
|
2998
|
+
updatedAt: stat?.updatedAt ?? Date.now(),
|
|
2999
|
+
});
|
|
3000
|
+
} catch (err) {
|
|
3001
|
+
const message = (err as Error)?.message ?? "Upload failed";
|
|
3002
|
+
const code = /quota|too large|exceed/i.test(message) ? 413 : 500;
|
|
3003
|
+
writeJson(response, code, { code: code === 413 ? "QUOTA" : "WRITE_FAILED", message });
|
|
3004
|
+
}
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
if (vfsMatch && request.method === "DELETE") {
|
|
3009
|
+
const rawPath = "/" + decodeURIComponent(vfsMatch[1] ?? "");
|
|
3010
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
3011
|
+
const engine = harness.storageEngine;
|
|
3012
|
+
if (!engine) {
|
|
3013
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
if (!isSafeVfsPath(rawPath) || rawPath === "/") {
|
|
3017
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
if (rawPath === MEMORY_VFILE_PATH) {
|
|
3021
|
+
writeJson(response, 400, {
|
|
3022
|
+
code: "RESERVED",
|
|
3023
|
+
message: "memory.md cannot be deleted; clear its contents instead.",
|
|
3024
|
+
});
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
try {
|
|
3028
|
+
const stat = await engine.vfs.stat(tenantId, rawPath);
|
|
3029
|
+
if (!stat) {
|
|
3030
|
+
writeJson(response, 404, { code: "NOT_FOUND", message: "Path not found" });
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
if (stat.type === "directory") {
|
|
3034
|
+
await engine.vfs.deleteDir(tenantId, rawPath, true);
|
|
3035
|
+
} else {
|
|
3036
|
+
await engine.vfs.deleteFile(tenantId, rawPath);
|
|
3037
|
+
}
|
|
3038
|
+
if (rawPath === "/skills" || rawPath.startsWith("/skills/")) {
|
|
3039
|
+
harness.invalidateSkillsForTenant(tenantId);
|
|
3040
|
+
}
|
|
3041
|
+
writeJson(response, 200, { ok: true, path: rawPath });
|
|
3042
|
+
} catch (err) {
|
|
3043
|
+
writeJson(response, 500, { code: "DELETE_FAILED", message: (err as Error)?.message ?? "Failed to delete" });
|
|
3044
|
+
}
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
if (pathname === "/api/vfs-archive" && request.method === "GET") {
|
|
3049
|
+
const dirPath = requestUrl.searchParams.get("path") ?? "/";
|
|
3050
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
3051
|
+
const engine = harness.storageEngine;
|
|
3052
|
+
if (!engine) {
|
|
3053
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
if (!isSafeVfsPath(dirPath)) {
|
|
3057
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
try {
|
|
3061
|
+
if (dirPath !== "/") {
|
|
3062
|
+
const stat = await engine.vfs.stat(tenantId, dirPath);
|
|
3063
|
+
if (!stat || stat.type !== "directory") {
|
|
3064
|
+
writeJson(response, 404, { code: "NOT_FOUND", message: "Directory not found" });
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
const entries: ZipEntry[] = [];
|
|
3069
|
+
const walk = async (dir: string, prefix: string): Promise<void> => {
|
|
3070
|
+
const children = await engine.vfs.readdir(tenantId, dir);
|
|
3071
|
+
for (const child of children) {
|
|
3072
|
+
if (dir === "/" && child.name === "memory.md") continue;
|
|
3073
|
+
const childPath = dir === "/" ? "/" + child.name : dir + "/" + child.name;
|
|
3074
|
+
const relName = prefix === "" ? child.name : prefix + "/" + child.name;
|
|
3075
|
+
if (child.type === "directory") {
|
|
3076
|
+
await walk(childPath, relName);
|
|
3077
|
+
} else if (child.type === "file") {
|
|
3078
|
+
const content = await engine.vfs.readFile(tenantId, childPath);
|
|
3079
|
+
const stat = await engine.vfs.stat(tenantId, childPath);
|
|
3080
|
+
entries.push({
|
|
3081
|
+
name: relName,
|
|
3082
|
+
content,
|
|
3083
|
+
mtime: stat?.updatedAt ? new Date(stat.updatedAt) : undefined,
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
await walk(dirPath, "");
|
|
3089
|
+
if (dirPath === "/") {
|
|
3090
|
+
const memory = await engine.memory.get(tenantId);
|
|
3091
|
+
entries.push({
|
|
3092
|
+
name: "memory.md",
|
|
3093
|
+
content: new Uint8Array(Buffer.from(memory.content, "utf-8")),
|
|
3094
|
+
mtime: memory.updatedAt > 0 ? new Date(memory.updatedAt) : undefined,
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
const archiveName = (dirPath === "/" ? "vfs" : (dirPath.split("/").pop() || "archive")) + ".zip";
|
|
3098
|
+
const zip = buildZip(entries);
|
|
3099
|
+
response.writeHead(200, {
|
|
3100
|
+
"Content-Type": "application/zip",
|
|
3101
|
+
"Content-Length": zip.length,
|
|
3102
|
+
"Content-Disposition": `attachment; filename="${archiveName}"`,
|
|
3103
|
+
"Cache-Control": "no-cache",
|
|
3104
|
+
});
|
|
3105
|
+
response.end(zip);
|
|
3106
|
+
} catch (err) {
|
|
3107
|
+
writeJson(response, 500, { code: "ARCHIVE_FAILED", message: (err as Error)?.message ?? "Failed to build archive" });
|
|
3108
|
+
}
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
if (pathname === "/api/vfs-list" && request.method === "GET") {
|
|
3113
|
+
const dirPath = requestUrl.searchParams.get("path") ?? "/";
|
|
3114
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
3115
|
+
const engine = harness.storageEngine;
|
|
3116
|
+
if (!engine) {
|
|
3117
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
if (!isSafeVfsPath(dirPath)) {
|
|
3121
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
try {
|
|
3125
|
+
if (dirPath !== "/") {
|
|
3126
|
+
const stat = await engine.vfs.stat(tenantId, dirPath);
|
|
3127
|
+
if (!stat || stat.type !== "directory") {
|
|
3128
|
+
writeJson(response, 404, { code: "NOT_FOUND", message: "Directory not found" });
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
const rawDirEntries = await engine.vfs.readdir(tenantId, dirPath);
|
|
3133
|
+
const dirEntries = dirPath === "/"
|
|
3134
|
+
? rawDirEntries.filter((e) => e.name !== "memory.md")
|
|
3135
|
+
: rawDirEntries;
|
|
3136
|
+
const entries: Array<{
|
|
3137
|
+
name: string;
|
|
3138
|
+
type: "file" | "directory" | "symlink";
|
|
3139
|
+
size: number;
|
|
3140
|
+
mimeType: string | null;
|
|
3141
|
+
updatedAt: number | null;
|
|
3142
|
+
}> = await Promise.all(dirEntries.map(async (entry) => {
|
|
3143
|
+
const childPath = dirPath === "/" ? "/" + entry.name : dirPath + "/" + entry.name;
|
|
3144
|
+
const stat = await engine.vfs.stat(tenantId, childPath);
|
|
3145
|
+
return {
|
|
3146
|
+
name: entry.name,
|
|
3147
|
+
type: entry.type,
|
|
3148
|
+
size: stat?.size ?? 0,
|
|
3149
|
+
mimeType: stat?.mimeType ?? null,
|
|
3150
|
+
updatedAt: stat?.updatedAt ?? null,
|
|
3151
|
+
};
|
|
3152
|
+
}));
|
|
3153
|
+
if (dirPath === "/") {
|
|
3154
|
+
const memory = await engine.memory.get(tenantId);
|
|
3155
|
+
entries.push({
|
|
3156
|
+
name: "memory.md",
|
|
3157
|
+
type: "file",
|
|
3158
|
+
size: Buffer.byteLength(memory.content, "utf-8"),
|
|
3159
|
+
mimeType: "text/markdown",
|
|
3160
|
+
updatedAt: memory.updatedAt > 0 ? memory.updatedAt : null,
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
entries.sort((a, b) => {
|
|
3164
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
3165
|
+
return a.name.localeCompare(b.name);
|
|
3166
|
+
});
|
|
3167
|
+
const usage = await engine.vfs.getUsage(tenantId);
|
|
3168
|
+
writeJson(response, 200, { path: dirPath, entries, usage });
|
|
3169
|
+
} catch (err) {
|
|
3170
|
+
writeJson(response, 500, { code: "READDIR_FAILED", message: (err as Error)?.message ?? "Failed to list directory" });
|
|
3171
|
+
}
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
if (pathname === "/api/vfs-mkdir" && request.method === "POST") {
|
|
3176
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
3177
|
+
const engine = harness.storageEngine;
|
|
3178
|
+
if (!engine) {
|
|
3179
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
try {
|
|
3183
|
+
const chunks: Buffer[] = [];
|
|
3184
|
+
for await (const chunk of request) chunks.push(chunk as Buffer);
|
|
3185
|
+
const body = JSON.parse(Buffer.concat(chunks).toString() || "{}") as { path?: string };
|
|
3186
|
+
const dirPath = body.path ?? "";
|
|
3187
|
+
if (!isSafeVfsPath(dirPath) || dirPath === "/") {
|
|
3188
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
if (dirPath === MEMORY_VFILE_PATH) {
|
|
3192
|
+
writeJson(response, 400, { code: "RESERVED", message: "memory.md is reserved" });
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
const existing = await engine.vfs.stat(tenantId, dirPath);
|
|
3196
|
+
if (existing) {
|
|
3197
|
+
writeJson(response, 409, { code: "EXISTS", message: "Path already exists" });
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
await engine.vfs.mkdir(tenantId, dirPath, true);
|
|
3201
|
+
if (dirPath === "/skills" || dirPath.startsWith("/skills/")) {
|
|
3202
|
+
harness.invalidateSkillsForTenant(tenantId);
|
|
3203
|
+
}
|
|
3204
|
+
writeJson(response, 200, { path: dirPath });
|
|
3205
|
+
} catch (err) {
|
|
3206
|
+
writeJson(response, 500, { code: "MKDIR_FAILED", message: (err as Error)?.message ?? "Failed to create directory" });
|
|
3207
|
+
}
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
|
|
2826
3211
|
if (pathname === "/api/slash-commands" && request.method === "GET") {
|
|
2827
3212
|
const skills: ApiSlashCommand[] = harness.listSkills().map((s) => ({
|
|
2828
3213
|
command: "/" + s.name,
|
|
@@ -3104,6 +3489,10 @@ export const createRequestHandler = async (options?: {
|
|
|
3104
3489
|
let checkpointedRun = false;
|
|
3105
3490
|
let runCancelled = false;
|
|
3106
3491
|
let runContinuationMessages: Message[] | undefined;
|
|
3492
|
+
// Snapshot of the harness's in-flight messages emitted with run:cancelled,
|
|
3493
|
+
// so the catch-path (executeConversationTurn threw) can still persist a
|
|
3494
|
+
// canonical history that includes the cancelled work.
|
|
3495
|
+
let cancelHarnessMessages: Message[] | undefined;
|
|
3107
3496
|
|
|
3108
3497
|
// Hoist stable ids for this turn. The same userMessage / assistantId is
|
|
3109
3498
|
// reused across every buildMessages() call so the in-flight assistant
|
|
@@ -3170,12 +3559,7 @@ export const createRequestHandler = async (options?: {
|
|
|
3170
3559
|
task: messageText,
|
|
3171
3560
|
conversationId,
|
|
3172
3561
|
tenantId: ctx.tenantId ?? undefined,
|
|
3173
|
-
parameters:
|
|
3174
|
-
...(bodyParameters ?? {}),
|
|
3175
|
-
...buildRecallParams({ ownerId, tenantId: ctx.tenantId, excludeConversationId: conversationId }),
|
|
3176
|
-
__activeConversationId: conversationId,
|
|
3177
|
-
__ownerId: ownerId,
|
|
3178
|
-
}, conversation),
|
|
3562
|
+
parameters: buildTurnParameters(conversation, { bodyParameters }),
|
|
3179
3563
|
messages: harnessMessages,
|
|
3180
3564
|
files: files.length > 0 ? files : undefined,
|
|
3181
3565
|
abortSignal: abortController.signal,
|
|
@@ -3200,6 +3584,7 @@ export const createRequestHandler = async (options?: {
|
|
|
3200
3584
|
}
|
|
3201
3585
|
if (event.type === "run:cancelled") {
|
|
3202
3586
|
runCancelled = true;
|
|
3587
|
+
if (event.messages) cancelHarnessMessages = event.messages;
|
|
3203
3588
|
}
|
|
3204
3589
|
if (event.type === "compaction:completed") {
|
|
3205
3590
|
if (event.compactedMessages) {
|
|
@@ -3327,7 +3712,17 @@ export const createRequestHandler = async (options?: {
|
|
|
3327
3712
|
if (abortController.signal.aborted || runCancelled) {
|
|
3328
3713
|
if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
|
|
3329
3714
|
conversation.messages = buildMessages();
|
|
3330
|
-
|
|
3715
|
+
// Keep _harnessMessages aligned with what the model just saw.
|
|
3716
|
+
// Without this, loadCanonicalHistory will hand the next turn a
|
|
3717
|
+
// pre-cancellation snapshot and the agent will have no memory of
|
|
3718
|
+
// the work it just did.
|
|
3719
|
+
applyTurnMetadata(conversation, {
|
|
3720
|
+
latestRunId,
|
|
3721
|
+
contextTokens: 0,
|
|
3722
|
+
contextWindow: 0,
|
|
3723
|
+
harnessMessages: cancelHarnessMessages,
|
|
3724
|
+
toolResultArchive: harness.getToolResultArchive(conversationId),
|
|
3725
|
+
}, { shouldRebuildCanonical: true });
|
|
3331
3726
|
await conversationStore.update(conversation);
|
|
3332
3727
|
}
|
|
3333
3728
|
if (!checkpointedRun) {
|
|
@@ -3456,6 +3851,8 @@ export const createRequestHandler = async (options?: {
|
|
|
3456
3851
|
const result = await runCronAgent(harness, task, conv.conversationId, historyMessages,
|
|
3457
3852
|
conv._toolResultArchive,
|
|
3458
3853
|
async (event) => { await telemetry.emit(event); },
|
|
3854
|
+
buildTurnParameters(conv),
|
|
3855
|
+
conv.tenantId,
|
|
3459
3856
|
);
|
|
3460
3857
|
|
|
3461
3858
|
const freshConv = await conversationStore.get(conv.conversationId);
|
|
@@ -3526,6 +3923,8 @@ export const createRequestHandler = async (options?: {
|
|
|
3526
3923
|
broadcastEvent(convId, event);
|
|
3527
3924
|
await telemetry.emit(event);
|
|
3528
3925
|
},
|
|
3926
|
+
buildTurnParameters(conversation),
|
|
3927
|
+
conversation.tenantId,
|
|
3529
3928
|
);
|
|
3530
3929
|
finishConversationStream(convId);
|
|
3531
3930
|
|
|
@@ -3673,6 +4072,9 @@ export const createRequestHandler = async (options?: {
|
|
|
3673
4072
|
harness, framedMessage, originConv.conversationId,
|
|
3674
4073
|
originConv.messages ?? [],
|
|
3675
4074
|
originConv._toolResultArchive,
|
|
4075
|
+
undefined,
|
|
4076
|
+
buildTurnParameters(originConv),
|
|
4077
|
+
originConv.tenantId,
|
|
3676
4078
|
);
|
|
3677
4079
|
if (result.response) {
|
|
3678
4080
|
try {
|
|
@@ -3705,7 +4107,13 @@ export const createRequestHandler = async (options?: {
|
|
|
3705
4107
|
`[reminder] ${reminder.task.slice(0, 80)} ${timestamp}`,
|
|
3706
4108
|
);
|
|
3707
4109
|
const convId = conversation.conversationId;
|
|
3708
|
-
const result = await runCronAgent(
|
|
4110
|
+
const result = await runCronAgent(
|
|
4111
|
+
harness, framedMessage, convId, [],
|
|
4112
|
+
undefined,
|
|
4113
|
+
undefined,
|
|
4114
|
+
buildTurnParameters(conversation),
|
|
4115
|
+
conversation.tenantId,
|
|
4116
|
+
);
|
|
3709
4117
|
const freshConv = await conversationStore.get(convId);
|
|
3710
4118
|
if (freshConv) {
|
|
3711
4119
|
freshConv.messages = buildCronMessages(framedMessage, [], result);
|
|
@@ -3741,6 +4149,7 @@ export const createRequestHandler = async (options?: {
|
|
|
3741
4149
|
handler._finishConversationStream = finishConversationStream;
|
|
3742
4150
|
handler._checkAndFireReminders = checkAndFireReminders;
|
|
3743
4151
|
handler._reminderPollIntervalMs = reminderPollWindowMs;
|
|
4152
|
+
handler._buildTurnParameters = buildTurnParameters;
|
|
3744
4153
|
|
|
3745
4154
|
// Recover stale subagent runs that were "running" when the server last stopped
|
|
3746
4155
|
orchestrator.recoverStaleSubagents().catch(err =>
|
|
@@ -3785,6 +4194,7 @@ export const startDevServer = async (
|
|
|
3785
4194
|
const activeRuns = handler._activeConversationRuns;
|
|
3786
4195
|
const deferredCallbacks = handler._pendingCallbackNeeded;
|
|
3787
4196
|
const runCallback = handler._processSubagentCallback;
|
|
4197
|
+
const buildParams = handler._buildTurnParameters;
|
|
3788
4198
|
if (!harnessRef || !store) return;
|
|
3789
4199
|
|
|
3790
4200
|
for (const [jobName, config] of entries) {
|
|
@@ -3845,6 +4255,8 @@ export const startDevServer = async (
|
|
|
3845
4255
|
const result = await runCronAgent(harnessRef, task, convId, historyMessages,
|
|
3846
4256
|
conversation._toolResultArchive,
|
|
3847
4257
|
broadcastCh ? (ev) => broadcastCh(convId, ev) : undefined,
|
|
4258
|
+
buildParams?.(conversation),
|
|
4259
|
+
conversation.tenantId,
|
|
3848
4260
|
);
|
|
3849
4261
|
handler._finishConversationStream?.(convId);
|
|
3850
4262
|
|
|
@@ -3913,6 +4325,8 @@ export const startDevServer = async (
|
|
|
3913
4325
|
const result = await runCronAgent(harnessRef, config.task, cronConvId, [],
|
|
3914
4326
|
conversation._toolResultArchive,
|
|
3915
4327
|
broadcast ? (ev) => broadcast(cronConvId!, ev) : undefined,
|
|
4328
|
+
buildParams?.(conversation),
|
|
4329
|
+
conversation.tenantId,
|
|
3916
4330
|
);
|
|
3917
4331
|
handler._finishConversationStream?.(cronConvId);
|
|
3918
4332
|
const freshConv = await store.get(cronConvId);
|
|
@@ -4054,6 +4468,12 @@ export const buildCli = (): Command => {
|
|
|
4054
4468
|
}
|
|
4055
4469
|
setLogLevel(level as typeof valid[number]);
|
|
4056
4470
|
}
|
|
4471
|
+
if (options.verbose) {
|
|
4472
|
+
// Surfaced to the request handler so the web UI can offer a toggle
|
|
4473
|
+
// between user-facing messages and the raw harness-message stream
|
|
4474
|
+
// sent to the model API.
|
|
4475
|
+
process.env.PONCHO_DEV_VERBOSE = "1";
|
|
4476
|
+
}
|
|
4057
4477
|
if (process.stdout.isTTY && !process.env.NO_COLOR) {
|
|
4058
4478
|
process.stdout.write("\n");
|
|
4059
4479
|
for (const line of getMascotLines()) process.stdout.write(`${line}\n`);
|