@dyx9/rosetta 0.0.0-stage → 0.0.1
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/.env.example +59 -0
- package/LICENSE +21 -0
- package/README.md +101 -2
- package/bin/rosetta.js +10 -0
- package/build/dist/assets/abnfDiagram-WEYOIA3C-CwZlxHkI.js +1 -0
- package/build/dist/assets/arc-B0x0q6O1.js +1 -0
- package/build/dist/assets/architecture-WOLXFQ4H-CkOpgBe5.js +1 -0
- package/build/dist/assets/architectureDiagram-ZHVYKT53-DT2OTBIB.js +36 -0
- package/build/dist/assets/array-BifhSqXX.js +1 -0
- package/build/dist/assets/bash-CLZ0U2zV.js +1 -0
- package/build/dist/assets/blockDiagram-RJAE7QA3-DXKyW4Qj.js +140 -0
- package/build/dist/assets/c4Diagram-XDPPPE54-CqHMZj3I.js +38 -0
- package/build/dist/assets/channel-BmjZSxDJ.js +1 -0
- package/build/dist/assets/chunk-2E7WHVMD-DTKlOCW9.js +88 -0
- package/build/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/build/dist/assets/chunk-4EMNKR2T-BsnKxZVc.js +1 -0
- package/build/dist/assets/chunk-55XCU76K-Ddul4yK8.js +1 -0
- package/build/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/build/dist/assets/chunk-7S6XSTFO-D73i4Kfs.js +2 -0
- package/build/dist/assets/chunk-AQDTSKZB-DJMpZsbr.js +72 -0
- package/build/dist/assets/chunk-DSNLSHJZ-KWPl82ns.js +129 -0
- package/build/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/build/dist/assets/chunk-HAGFOTED-Co5xWe5K.js +62 -0
- package/build/dist/assets/chunk-HNE4Q36P-D9ufLjJL.js +1 -0
- package/build/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/build/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/build/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/build/dist/assets/chunk-NQOKRVIF-DuVeWA2D.js +213 -0
- package/build/dist/assets/chunk-UHW7PFBP-DrzHcxiz.js +1 -0
- package/build/dist/assets/chunk-VQ5W62QI-tyK9wvZy.js +1 -0
- package/build/dist/assets/chunk-WQQL4CDK-BtfOu2ju.js +10 -0
- package/build/dist/assets/chunk-XXDRQBXY-BFig8xVb.js +1 -0
- package/build/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/build/dist/assets/chunk-YZO67HFG-Cw5eVjq1.js +1 -0
- package/build/dist/assets/chunk-ZAUX2LTX-CIpKyMEe.js +1 -0
- package/build/dist/assets/chunk-ZCHMQSOR-Djve8pwS.js +2 -0
- package/build/dist/assets/classDiagram-v2-4MYWR3UA-D4XAcUCd.js +217 -0
- package/build/dist/assets/cose-bilkent-JH36ORCC-DHRGyJqU.js +1 -0
- package/build/dist/assets/cynefin-EF2NZ3EQ-Cv-uxuqE.js +1 -0
- package/build/dist/assets/cynefinDiagram-WF24UDRP-MFe-UzcY.js +62 -0
- package/build/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/build/dist/assets/dagre-NK4GIZVX-BsY0-Q-N.js +4 -0
- package/build/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/build/dist/assets/diagram-CBK3MWTC-9zfXZ0bX.js +24 -0
- package/build/dist/assets/diagram-I5XIIV2D-CfWjvEof.js +24 -0
- package/build/dist/assets/diagram-ODIL2UCW-BpJaYO2i.js +30 -0
- package/build/dist/assets/diagram-PFBEJUXE-CtpxT_FQ.js +3 -0
- package/build/dist/assets/diagram-RJA3I4VV-DuLQrOcT.js +41 -0
- package/build/dist/assets/diagram-TYUNFSNR-w6Mohdl5.js +200 -0
- package/build/dist/assets/dist-DI7WHQIh.js +1 -0
- package/build/dist/assets/ebnfDiagram-V4P3TTPT-DlwHfKyL.js +1 -0
- package/build/dist/assets/elk-QONTBCTQ-DEvtD9nx.js +27 -0
- package/build/dist/assets/erDiagram-KYQIGQAA-DQuhkec3.js +99 -0
- package/build/dist/assets/eventmodeling-K75KTNOO-BO57PAOs.js +1 -0
- package/build/dist/assets/flowDiagram-IUGLLWBU-B_Ib7Avh.js +1 -0
- package/build/dist/assets/ganttDiagram-R7GPNTJY-CDGbtnUY.js +292 -0
- package/build/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/build/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/build/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/build/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/build/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/build/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/build/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/build/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/build/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/build/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/build/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/build/dist/assets/gitGraph-VSP46ZUC-D9ZZKP8z.js +1 -0
- package/build/dist/assets/gitGraphDiagram-PDYL7XHY-Da8Oypep.js +106 -0
- package/build/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/build/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/build/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/build/dist/assets/index-Cf0aBmwz.css +2 -0
- package/build/dist/assets/index-Ct3brajV.js +471 -0
- package/build/dist/assets/info-OHQRW6UA-BF836x6L.js +1 -0
- package/build/dist/assets/infoDiagram-MVDRKAX5-DpOoH-IR.js +2 -0
- package/build/dist/assets/init-C-OQMol4.js +1 -0
- package/build/dist/assets/ishikawaDiagram-HPEHV2GE-DT-Ft7vW.js +70 -0
- package/build/dist/assets/javascript-BgS3c2Ky.js +1 -0
- package/build/dist/assets/journeyDiagram-BOSHW7O7-BAF7tJ1H.js +139 -0
- package/build/dist/assets/json-qhed-kSA.js +1 -0
- package/build/dist/assets/kanban-definition-FNSA4HC5-B_w2r2eo.js +89 -0
- package/build/dist/assets/katex-B_JcIwqj.js +257 -0
- package/build/dist/assets/line-DMjgRylK.js +1 -0
- package/build/dist/assets/linear-BEdVl1PM.js +1 -0
- package/build/dist/assets/mermaid-parser.core-BDmgiU7m.js +7 -0
- package/build/dist/assets/mermaid.core-8Kj_dqOs.js +44 -0
- package/build/dist/assets/mindmap-definition-6OY57UG3-DOcaArfN.js +98 -0
- package/build/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/build/dist/assets/packet-JDAUHWVQ-zWLPVpvX.js +1 -0
- package/build/dist/assets/path-fybaL0A-.js +1 -0
- package/build/dist/assets/pegDiagram-ECIUJNBX-sYwsFyAl.js +1 -0
- package/build/dist/assets/pie-XZMESJXO-B-h0LV9S.js +1 -0
- package/build/dist/assets/pieDiagram-GNMPY74T-FToCIvky.js +39 -0
- package/build/dist/assets/python-gzcpVVnB.js +1 -0
- package/build/dist/assets/quadrantDiagram-D57K4MC3-tiXtbFLP.js +7 -0
- package/build/dist/assets/radar-ABXABTNO--ew_ZFJl.js +1 -0
- package/build/dist/assets/railroad-I3PHUGI6-cxsf_ZsE.js +1 -0
- package/build/dist/assets/railroad-abnf-LNEFI6M7-BIK-9C0Z.js +1 -0
- package/build/dist/assets/railroad-ebnf-DWYD2UWJ-BHw1M8VQ.js +1 -0
- package/build/dist/assets/railroad-peg-JR7BVNR7-MHCdZHAZ.js +1 -0
- package/build/dist/assets/railroadDiagram-Y72NRBL7-wjsZpnJA.js +1 -0
- package/build/dist/assets/requirementDiagram-EQYSW5RA-CHkzAP3w.js +84 -0
- package/build/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/build/dist/assets/sankeyDiagram-SY24JHO5-BBLq6ndO.js +40 -0
- package/build/dist/assets/sequenceDiagram-BLEHNHQU-DW_h9VZQ.js +169 -0
- package/build/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/build/dist/assets/src-MbiMZlpk.js +1 -0
- package/build/dist/assets/stateDiagram-v2-A6IESKWZ-BgpDkVUs.js +291 -0
- package/build/dist/assets/swimlanes-B3ENRKFQ-DJHWFYmN.js +1 -0
- package/build/dist/assets/swimlanesDiagram-AYQRV4AK-Cq_MGOxD.js +8 -0
- package/build/dist/assets/timeline-definition-JXYKGLGB-DQ-XYDnd.js +120 -0
- package/build/dist/assets/treeView-D4JQ5SDB-ydIf6KAy.js +1 -0
- package/build/dist/assets/treemap-SAJKECNS-CB6tZuFJ.js +1 -0
- package/build/dist/assets/typescript-bsJCZSQ-.js +1 -0
- package/build/dist/assets/usecaseDiagram-WPOLBNNX-D_A5G5wv.js +328 -0
- package/build/dist/assets/vennDiagram-Q3Y3IDCW-j2yUit_4.js +34 -0
- package/build/dist/assets/wardley-7MLQ67FV-BztBXEva.js +1 -0
- package/build/dist/assets/wardleyDiagram-IVAIDKWA-CyrXADWS.js +78 -0
- package/build/dist/assets/xychartDiagram-4URNHDU4-BM2yVuEH.js +7 -0
- package/build/dist/favicon.svg +1 -0
- package/build/dist/icons.svg +24 -0
- package/build/dist/index.html +14 -0
- package/build/server/agent/conversation.js +244 -0
- package/build/server/agent/harness.js +435 -0
- package/build/server/agent/sessions.js +222 -0
- package/build/server/agent/turn.js +56 -0
- package/build/server/config.js +92 -0
- package/build/server/http/auth.js +31 -0
- package/build/server/http/chat.js +67 -0
- package/build/server/http/feedback.js +30 -0
- package/build/server/http/routes.js +231 -0
- package/build/server/http/stream.js +39 -0
- package/build/server/http/util.js +40 -0
- package/build/server/index.js +112 -0
- package/build/server/records/operations.js +53 -0
- package/build/server/records/recording.js +186 -0
- package/build/server/records/turn-recording.js +151 -0
- package/build/server/stone/file-boundary.js +57 -0
- package/build/server/stone/prompt.js +83 -0
- package/build/server/stone/stone.js +245 -0
- package/build/server/stone/verify.js +141 -0
- package/build/src/shared/protocol.js +25 -0
- package/build/stones/example/AGENTS.md +25 -0
- package/build/stones/example/checks/hotspot.py +74 -0
- package/build/stones/example/checks/syntax.py +13 -0
- package/build/stones/example/constraints/rig-limits.md +15 -0
- package/build/stones/example/docs/reference.md +26 -0
- package/build/stones/example/examples/good/crossfade.py +15 -0
- package/build/stones/example/examples/violations/hotspot.py +17 -0
- package/build/stones/example/rosetta.json +23 -0
- package/build/stones/example/skills/writing-cues/SKILL.md +21 -0
- package/package.json +74 -4
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation turns, independent of any transport.
|
|
3
|
+
*
|
|
4
|
+
* A caller admits a request, delivers progress however it can, and receives one
|
|
5
|
+
* final result. HTTP (http/chat.ts) is one such caller; tests call this directly.
|
|
6
|
+
*/
|
|
7
|
+
import { SESSION_NOT_FOUND, } from "./sessions.js";
|
|
8
|
+
import { startTurnRecording, } from "../records/turn-recording.js";
|
|
9
|
+
import { TurnError, stopResult, validTurnId, } from "./turn.js";
|
|
10
|
+
export const DEFAULT_TURN_LIMITS = {
|
|
11
|
+
timeoutMs: 180_000,
|
|
12
|
+
abortGraceMs: 5_000,
|
|
13
|
+
};
|
|
14
|
+
export const MAX_MESSAGE_CHARS = 32_000;
|
|
15
|
+
const REJECTIONS = {
|
|
16
|
+
invalid_message: "A message between 1 and 32,000 characters is required",
|
|
17
|
+
invalid_turn_id: "A valid turnId is required",
|
|
18
|
+
session_not_found: SESSION_NOT_FOUND,
|
|
19
|
+
session_busy: "This session is already answering",
|
|
20
|
+
turn_id_reused: "This turnId has already been used",
|
|
21
|
+
};
|
|
22
|
+
const rejection = (reason) => ({
|
|
23
|
+
reason,
|
|
24
|
+
message: REJECTIONS[reason],
|
|
25
|
+
});
|
|
26
|
+
const rejected = (reason) => ({
|
|
27
|
+
accepted: false,
|
|
28
|
+
rejection: rejection(reason),
|
|
29
|
+
});
|
|
30
|
+
/** Decides whether a question may start. The turn it admits does the rest. */
|
|
31
|
+
export class Conversations {
|
|
32
|
+
#sessions;
|
|
33
|
+
#services;
|
|
34
|
+
constructor(sessions, recordings, operations, limits = DEFAULT_TURN_LIMITS) {
|
|
35
|
+
this.#sessions = sessions;
|
|
36
|
+
this.#services = { sessions, recordings, operations, limits };
|
|
37
|
+
}
|
|
38
|
+
/** Early check before a transport waits for the full request. Admission checks again. */
|
|
39
|
+
eligibility(sessionId) {
|
|
40
|
+
const session = this.#sessions.find(sessionId);
|
|
41
|
+
if (!session)
|
|
42
|
+
return rejection("session_not_found");
|
|
43
|
+
if (session.busy)
|
|
44
|
+
return rejection("session_busy");
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Validates the request against the session's current state and takes exclusive ownership of its turn. */
|
|
48
|
+
admit(request) {
|
|
49
|
+
const message = typeof request.message === "string" ? request.message.trim() : "";
|
|
50
|
+
if (!message || message.length > MAX_MESSAGE_CHARS)
|
|
51
|
+
return rejected("invalid_message");
|
|
52
|
+
if (!validTurnId(request.turnId))
|
|
53
|
+
return rejected("invalid_turn_id");
|
|
54
|
+
const session = this.#sessions.find(request.sessionId);
|
|
55
|
+
if (!session)
|
|
56
|
+
return rejected("session_not_found");
|
|
57
|
+
if (session.turnRecordings.has(request.turnId))
|
|
58
|
+
return rejected("turn_id_reused");
|
|
59
|
+
const turn = session.beginTurn(request.turnId);
|
|
60
|
+
if (!turn)
|
|
61
|
+
return rejected("session_busy");
|
|
62
|
+
const recording = startTurnRecording(session.recording, message);
|
|
63
|
+
session.turnRecordings.set(request.turnId, recording);
|
|
64
|
+
return {
|
|
65
|
+
accepted: true,
|
|
66
|
+
turn: new AcceptedTurn(this.#services, session, turn, message, recording),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* An admitted turn owns its session until it finishes. Admission starts the deadline
|
|
72
|
+
* and run() starts the agent. A turn cancelled before run() still finishes: it is
|
|
73
|
+
* recorded and releases its session without the agent being asked.
|
|
74
|
+
*/
|
|
75
|
+
export class AcceptedTurn {
|
|
76
|
+
id;
|
|
77
|
+
#services;
|
|
78
|
+
#session;
|
|
79
|
+
#turn;
|
|
80
|
+
#message;
|
|
81
|
+
#recording;
|
|
82
|
+
#result;
|
|
83
|
+
/** Called by run(); until then the turn waits, or finishes if it is cancelled. */
|
|
84
|
+
#start;
|
|
85
|
+
#deadline;
|
|
86
|
+
/** True once the agent has been asked; only then does a cancel have to stop it. */
|
|
87
|
+
#agentStarted = false;
|
|
88
|
+
/** False once the result is decided; anything the agent sends after that is dropped. */
|
|
89
|
+
#forwarding = true;
|
|
90
|
+
#replyChars = 0;
|
|
91
|
+
#tools = new Set();
|
|
92
|
+
constructor(services, session, turn, message, recording) {
|
|
93
|
+
this.id = turn.id;
|
|
94
|
+
this.#services = services;
|
|
95
|
+
this.#session = session;
|
|
96
|
+
this.#turn = turn;
|
|
97
|
+
this.#message = message;
|
|
98
|
+
this.#recording = recording;
|
|
99
|
+
const start = new Promise((resolve) => {
|
|
100
|
+
this.#start = resolve;
|
|
101
|
+
});
|
|
102
|
+
this.#deadline = setTimeout(() => turn.cancel("timeout"), services.limits.timeoutMs);
|
|
103
|
+
this.#result = Promise.race([start, turn.cancelled]).then((onProgress) => this.#run(onProgress ?? (() => { })));
|
|
104
|
+
// A caller may abandon this turn without ever observing its result.
|
|
105
|
+
void this.#result.catch((error) => {
|
|
106
|
+
console.error("Turn finalization failed", error);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Cancels this turn only; no effect once it has finished. */
|
|
110
|
+
cancel(reason) {
|
|
111
|
+
if (this.#session.activeTurn === this.#turn)
|
|
112
|
+
this.#turn.cancel(reason);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolves with the single final result after recording, measurement, and session
|
|
116
|
+
* release. Progress stops before it resolves; repeated calls share the one result.
|
|
117
|
+
*/
|
|
118
|
+
run(onProgress = () => { }) {
|
|
119
|
+
this.#start(onProgress);
|
|
120
|
+
return this.#result;
|
|
121
|
+
}
|
|
122
|
+
/** One pass: the question unless already cancelled, a stop if it is cancelled, then the record and the release. */
|
|
123
|
+
async #run(onProgress) {
|
|
124
|
+
const { sessions } = this.#services;
|
|
125
|
+
const turn = this.#turn;
|
|
126
|
+
let result = {
|
|
127
|
+
outcome: "failed",
|
|
128
|
+
message: "The response could not be completed.",
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
if (turn.stopReason) {
|
|
132
|
+
result = stopResult(turn.stopReason);
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
const asked = this.#ask(onProgress);
|
|
136
|
+
const answer = await Promise.race([asked, turn.cancelled]);
|
|
137
|
+
if (turn.stopReason) {
|
|
138
|
+
this.#forwarding = false;
|
|
139
|
+
result = stopResult(turn.stopReason);
|
|
140
|
+
if (this.#agentStarted && !(await this.#stopAgent(asked))) {
|
|
141
|
+
sessions.destroy(this.#session.id, "unresponsive_agent");
|
|
142
|
+
result.message +=
|
|
143
|
+
" This session was ended; start a new conversation to continue.";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (answer)
|
|
147
|
+
result = answer;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
result = {
|
|
151
|
+
outcome: "failed",
|
|
152
|
+
message: error instanceof Error ? error.message : "The response failed.",
|
|
153
|
+
};
|
|
154
|
+
sessions.destroy(this.#session.id, "response_failure");
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
this.#forwarding = false;
|
|
158
|
+
clearTimeout(this.#deadline);
|
|
159
|
+
try {
|
|
160
|
+
this.#record(result);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
this.#session.endTurn(turn);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
/** Asks the agent and resolves with what happened. Never rejects. */
|
|
169
|
+
#ask(onProgress) {
|
|
170
|
+
const turn = this.#turn;
|
|
171
|
+
// Started on a later tick, so a turn cancelled straight after run() never reaches the agent.
|
|
172
|
+
return Promise.resolve()
|
|
173
|
+
.then(async () => {
|
|
174
|
+
if (turn.stopReason)
|
|
175
|
+
return stopResult(turn.stopReason);
|
|
176
|
+
this.#agentStarted = true;
|
|
177
|
+
await this.#session.agent.ask(this.#message, (event) => this.#forward(event, onProgress), (event) => {
|
|
178
|
+
if (this.#forwarding && !turn.stopReason)
|
|
179
|
+
this.#recording.event(event);
|
|
180
|
+
});
|
|
181
|
+
return { outcome: "completed" };
|
|
182
|
+
})
|
|
183
|
+
.catch((error) => ({
|
|
184
|
+
outcome: error instanceof TurnError ? error.outcome : "failed",
|
|
185
|
+
message: error instanceof Error ? error.message : "The agent failed.",
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
/** Passes one progress event on, unless the result is already decided. */
|
|
189
|
+
#forward(event, onProgress) {
|
|
190
|
+
if (!this.#forwarding || this.#turn.stopReason)
|
|
191
|
+
return;
|
|
192
|
+
if (event.t === "delta")
|
|
193
|
+
this.#replyChars += event.text.length;
|
|
194
|
+
if (event.t === "tool" && event.state === "running")
|
|
195
|
+
this.#tools.add(event.name);
|
|
196
|
+
// A recipient that cannot take progress has gone, so its turn ends as a disconnect.
|
|
197
|
+
try {
|
|
198
|
+
onProgress(event);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.error("Turn progress delivery failed", error);
|
|
202
|
+
this.#turn.cancel("disconnected");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Stops the agent after a cancel. Both the question and the abort must settle
|
|
207
|
+
* before the agent can take another question; false means they did not within
|
|
208
|
+
* the grace period, or the abort itself failed.
|
|
209
|
+
*/
|
|
210
|
+
async #stopAgent(asked) {
|
|
211
|
+
const stopped = Promise.resolve()
|
|
212
|
+
.then(() => this.#session.agent.abort())
|
|
213
|
+
.then(() => asked)
|
|
214
|
+
.then(() => true, () => false);
|
|
215
|
+
let grace;
|
|
216
|
+
const outOfTime = new Promise((resolve) => {
|
|
217
|
+
grace = setTimeout(() => resolve(false), this.#services.limits.abortGraceMs);
|
|
218
|
+
});
|
|
219
|
+
const clean = await Promise.race([stopped, outOfTime]);
|
|
220
|
+
clearTimeout(grace);
|
|
221
|
+
return clean;
|
|
222
|
+
}
|
|
223
|
+
/** Writes the result to the transcript, the admin totals, and the recent-question list. */
|
|
224
|
+
#record(result) {
|
|
225
|
+
const { operations, recordings } = this.#services;
|
|
226
|
+
const { outcome, message } = result;
|
|
227
|
+
const measurement = this.#recording.finish({
|
|
228
|
+
outcome,
|
|
229
|
+
error: outcome === "completed" ? undefined : message,
|
|
230
|
+
});
|
|
231
|
+
if (measurement) {
|
|
232
|
+
this.#session.contextPercent = measurement.contextPercent;
|
|
233
|
+
operations.recordTurn(outcome, measurement, message);
|
|
234
|
+
}
|
|
235
|
+
recordings.logQuestion({
|
|
236
|
+
outcome,
|
|
237
|
+
at: new Date().toISOString(),
|
|
238
|
+
question: this.#message,
|
|
239
|
+
replyChars: this.#replyChars,
|
|
240
|
+
tools: [...this.#tools],
|
|
241
|
+
...(outcome !== "completed" ? { error: message ?? outcome } : {}),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only file that imports pi.
|
|
3
|
+
*
|
|
4
|
+
* Everything else in the server sees these two interfaces. If the harness is ever
|
|
5
|
+
* swapped, this file is what gets rewritten.
|
|
6
|
+
*/
|
|
7
|
+
import { realpathSync } from "node:fs";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { relative, join, resolve as resolvePath } from "node:path";
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
import { createAgentSession, createReadToolDefinition, createGrepToolDefinition, createFindToolDefinition, createLsToolDefinition, defineTool, createExtensionRuntime, loadSkillsFromDir, getAgentDir, ModelRuntime, resolveCliModel, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { CHECK_TOOL, } from "../../src/shared/protocol.js";
|
|
13
|
+
import {} from "../stone/stone.js";
|
|
14
|
+
import { runChecks } from "../stone/verify.js";
|
|
15
|
+
import {} from "../records/recording.js";
|
|
16
|
+
import {} from "../records/turn-recording.js";
|
|
17
|
+
import { TurnError } from "./turn.js";
|
|
18
|
+
import { contains, stoneToolPath } from "../stone/file-boundary.js";
|
|
19
|
+
/** Read-only. pi's defaults include bash, edit and write, which a question-answering assistant must not have. */
|
|
20
|
+
const TOOLS = ["read", "grep", "find", "ls"];
|
|
21
|
+
/** Custom definitions override Pi's built-ins under the same allowlisted names. */
|
|
22
|
+
export function createStoneTools(stonePath) {
|
|
23
|
+
const root = realpathSync(stonePath);
|
|
24
|
+
function guard(tool) {
|
|
25
|
+
return defineTool({
|
|
26
|
+
...tool,
|
|
27
|
+
description: `${tool.description} Only files inside the active stone are accessible. Directory scans cannot include symbolic links.`,
|
|
28
|
+
async execute(id, args, signal, onUpdate, ctx) {
|
|
29
|
+
const params = args;
|
|
30
|
+
const scan = tool.name === "read"
|
|
31
|
+
? "none"
|
|
32
|
+
: tool.name === "ls"
|
|
33
|
+
? "shallow"
|
|
34
|
+
: "recursive";
|
|
35
|
+
const path = await stoneToolPath(root, params.path ?? ".", scan, signal);
|
|
36
|
+
return tool.execute(id, { ...params, path }, signal, onUpdate, ctx);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return [
|
|
41
|
+
guard(createReadToolDefinition(root)),
|
|
42
|
+
guard(createGrepToolDefinition(root)),
|
|
43
|
+
guard(createFindToolDefinition(root)),
|
|
44
|
+
guard(createLsToolDefinition(root)),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
export async function createHarness(stone, modelPattern) {
|
|
48
|
+
const modelRuntime = await ModelRuntime.create();
|
|
49
|
+
const { model, thinkingLevel, source } = await resolveModel(modelRuntime, modelPattern);
|
|
50
|
+
const loader = createStoneLoader(stone);
|
|
51
|
+
const skills = loader.getSkills().skills;
|
|
52
|
+
// Reported rather than assumed: the check tool is only enabled when the
|
|
53
|
+
// stone declares checkers, and the banner should say what is actually on.
|
|
54
|
+
const tools = stone.checks.length ? [...TOOLS, CHECK_TOOL] : TOOLS;
|
|
55
|
+
const modelName = model ? `${model.provider}/${model.id}` : null;
|
|
56
|
+
return {
|
|
57
|
+
stone,
|
|
58
|
+
tools,
|
|
59
|
+
model: modelName ? `${modelName} (${source})` : "unresolved",
|
|
60
|
+
skills: skills.map((skill) => skill.name),
|
|
61
|
+
checks: stone.checks.map((check) => check.name),
|
|
62
|
+
async create() {
|
|
63
|
+
const findResource = createResourceFinder(stone, skills);
|
|
64
|
+
// The budget is per question, not per session: each new turn starts fresh.
|
|
65
|
+
const attempts = { failed: 0 };
|
|
66
|
+
const checkTool = stone.checks.length
|
|
67
|
+
? createCheckTool(stone, attempts)
|
|
68
|
+
: undefined;
|
|
69
|
+
const { session } = await createAgentSession({
|
|
70
|
+
resourceLoader: loader,
|
|
71
|
+
modelRuntime,
|
|
72
|
+
sessionManager: SessionManager.inMemory(stone.path),
|
|
73
|
+
cwd: stone.path,
|
|
74
|
+
tools,
|
|
75
|
+
customTools: [
|
|
76
|
+
...createStoneTools(stone.path),
|
|
77
|
+
...(checkTool ? [checkTool] : []),
|
|
78
|
+
],
|
|
79
|
+
...(model ? { model } : {}),
|
|
80
|
+
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
81
|
+
});
|
|
82
|
+
return new PiAgent(session, {
|
|
83
|
+
metadata: {
|
|
84
|
+
model: modelName,
|
|
85
|
+
stone: stone.agentName,
|
|
86
|
+
alwaysInContext: stone.context.map((file) => file.path),
|
|
87
|
+
},
|
|
88
|
+
model,
|
|
89
|
+
findResource,
|
|
90
|
+
attempts,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** One conversation with the model, held in a Pi session. */
|
|
96
|
+
class PiAgent {
|
|
97
|
+
metadata;
|
|
98
|
+
#session;
|
|
99
|
+
#model;
|
|
100
|
+
#findResource;
|
|
101
|
+
#attempts;
|
|
102
|
+
constructor(session, options) {
|
|
103
|
+
this.#session = session;
|
|
104
|
+
this.metadata = options.metadata;
|
|
105
|
+
this.#model = options.model;
|
|
106
|
+
this.#findResource = options.findResource;
|
|
107
|
+
this.#attempts = options.attempts;
|
|
108
|
+
}
|
|
109
|
+
async ask(message, onEvent, record = () => { }) {
|
|
110
|
+
this.#attempts.failed = 0;
|
|
111
|
+
const question = { onEvent, record, reads: new Map() };
|
|
112
|
+
this.#recordContextUsage(record);
|
|
113
|
+
const unsubscribe = this.#session.subscribe((event) => this.#translate(event, question));
|
|
114
|
+
try {
|
|
115
|
+
await this.#session.prompt(message);
|
|
116
|
+
const { stopReason, errorMessage } = question.lastResponse ?? {};
|
|
117
|
+
const outcome = turnOutcome(stopReason);
|
|
118
|
+
record({
|
|
119
|
+
type: "provider_outcome",
|
|
120
|
+
data: { outcome, error: errorMessage },
|
|
121
|
+
});
|
|
122
|
+
if (outcome !== "completed")
|
|
123
|
+
throw new TurnError(outcome, errorMessage ||
|
|
124
|
+
(stopReason === "length"
|
|
125
|
+
? "The model reached its output limit. This response is incomplete."
|
|
126
|
+
: `Agent turn ${outcome}`));
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
this.#recordContextUsage(record);
|
|
130
|
+
unsubscribe();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async abort() {
|
|
134
|
+
await this.#session.abort();
|
|
135
|
+
}
|
|
136
|
+
dispose() {
|
|
137
|
+
this.#session.dispose();
|
|
138
|
+
}
|
|
139
|
+
/** Passes one Pi event on as what the browser shows and what the recording keeps. */
|
|
140
|
+
#translate(event, question) {
|
|
141
|
+
const { onEvent, record, reads } = question;
|
|
142
|
+
switch (event.type) {
|
|
143
|
+
case "message_update": {
|
|
144
|
+
if (event.assistantMessageEvent.type !== "text_delta")
|
|
145
|
+
return;
|
|
146
|
+
const text = event.assistantMessageEvent.delta;
|
|
147
|
+
record({ type: "assistant_delta", data: { text } });
|
|
148
|
+
onEvent({ t: "delta", text });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
case "message_end": {
|
|
152
|
+
if (event.message.role !== "assistant")
|
|
153
|
+
return;
|
|
154
|
+
question.lastResponse = event.message;
|
|
155
|
+
record({
|
|
156
|
+
type: "assistant_message",
|
|
157
|
+
data: measureResponse(event.message, this.#model),
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
case "tool_execution_start": {
|
|
162
|
+
record({ type: "tool_used", data: { name: event.toolName } });
|
|
163
|
+
if (event.toolName === "read" && typeof event.args?.path === "string") {
|
|
164
|
+
const resource = this.#findResource(event.args.path);
|
|
165
|
+
if (resource)
|
|
166
|
+
reads.set(event.toolCallId, resource);
|
|
167
|
+
}
|
|
168
|
+
onEvent({ t: "tool", name: event.toolName, state: "running" });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
case "tool_execution_end": {
|
|
172
|
+
// A read is recorded as a document or a skill only once it has succeeded.
|
|
173
|
+
const resource = reads.get(event.toolCallId);
|
|
174
|
+
if (resource && !event.isError) {
|
|
175
|
+
record(resource.skill
|
|
176
|
+
? { type: "skill_loaded", data: { skill: resource.skill } }
|
|
177
|
+
: { type: "document_read", data: { path: resource.path } });
|
|
178
|
+
}
|
|
179
|
+
reads.delete(event.toolCallId);
|
|
180
|
+
onEvent({
|
|
181
|
+
t: "tool",
|
|
182
|
+
name: event.toolName,
|
|
183
|
+
state: "done",
|
|
184
|
+
isError: event.isError,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
case "compaction_end":
|
|
189
|
+
this.#recordContextUsage(record);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
#recordContextUsage(record) {
|
|
194
|
+
let percent = null;
|
|
195
|
+
try {
|
|
196
|
+
percent = this.#session.getContextUsage()?.percent ?? null;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Unknown usage is recorded as null.
|
|
200
|
+
}
|
|
201
|
+
record({ type: "context_usage", data: { percent } });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Pi's stop reason for the last response, as the outcome of the turn. */
|
|
205
|
+
function turnOutcome(stopReason) {
|
|
206
|
+
if (stopReason === "stop")
|
|
207
|
+
return "completed";
|
|
208
|
+
if (stopReason === "error")
|
|
209
|
+
return "failed";
|
|
210
|
+
if (stopReason === "aborted")
|
|
211
|
+
return "stopped";
|
|
212
|
+
return "incomplete";
|
|
213
|
+
}
|
|
214
|
+
/** A finished response as the recording keeps it: its text, and its usage and cost when they are real. */
|
|
215
|
+
function measureResponse(message, model) {
|
|
216
|
+
const reported = message.usage;
|
|
217
|
+
// Pi initializes missing usage to zero; do not pretend those placeholders are measured.
|
|
218
|
+
const usage = reported &&
|
|
219
|
+
[
|
|
220
|
+
reported.input,
|
|
221
|
+
reported.output,
|
|
222
|
+
reported.cacheRead,
|
|
223
|
+
reported.cacheWrite,
|
|
224
|
+
reported.totalTokens,
|
|
225
|
+
].every(Number.isFinite) &&
|
|
226
|
+
reported.totalTokens > 0
|
|
227
|
+
? {
|
|
228
|
+
input: reported.input,
|
|
229
|
+
output: reported.output,
|
|
230
|
+
cacheRead: reported.cacheRead,
|
|
231
|
+
cacheWrite: reported.cacheWrite,
|
|
232
|
+
totalTokens: reported.totalTokens,
|
|
233
|
+
}
|
|
234
|
+
: null;
|
|
235
|
+
// Use Pi's per-response estimate: it already accounts for cache and pricing tiers.
|
|
236
|
+
const cost = reported?.cost;
|
|
237
|
+
const estimatedCostUsd = usage &&
|
|
238
|
+
hasRealPrices(model) &&
|
|
239
|
+
model?.provider === message.provider &&
|
|
240
|
+
model.id === message.model &&
|
|
241
|
+
cost &&
|
|
242
|
+
[
|
|
243
|
+
cost.input,
|
|
244
|
+
cost.output,
|
|
245
|
+
cost.cacheRead,
|
|
246
|
+
cost.cacheWrite,
|
|
247
|
+
cost.total,
|
|
248
|
+
].every((value) => Number.isFinite(value) && value >= 0)
|
|
249
|
+
? cost.total
|
|
250
|
+
: null;
|
|
251
|
+
return {
|
|
252
|
+
text: message.content
|
|
253
|
+
.filter((part) => part.type === "text")
|
|
254
|
+
.map((part) => part.text)
|
|
255
|
+
.join(""),
|
|
256
|
+
usage,
|
|
257
|
+
estimatedCostUsd,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/** All-zero catalog prices can be placeholders (e.g. subscription/custom models). */
|
|
261
|
+
function hasRealPrices(model) {
|
|
262
|
+
const rates = model?.cost;
|
|
263
|
+
return Boolean(rates &&
|
|
264
|
+
[rates, ...(rates.tiers ?? [])].some((rate) => [rate.input, rate.output, rate.cacheRead, rate.cacheWrite].some((value) => Number.isFinite(value) && value > 0)));
|
|
265
|
+
}
|
|
266
|
+
/** Finds which stone file a `read` call names. A failed or external read names none. */
|
|
267
|
+
function createResourceFinder(stone, skills) {
|
|
268
|
+
const catalog = skills.map((skill) => ({
|
|
269
|
+
name: skill.name,
|
|
270
|
+
path: relative(stone.path, skill.filePath),
|
|
271
|
+
}));
|
|
272
|
+
const root = realpathSync(stone.path);
|
|
273
|
+
return (requested) => {
|
|
274
|
+
try {
|
|
275
|
+
const resolved = realpathSync(resolvePath(stone.path, requested));
|
|
276
|
+
if (!contains(root, resolved))
|
|
277
|
+
return undefined;
|
|
278
|
+
const path = relative(root, resolved);
|
|
279
|
+
return {
|
|
280
|
+
path,
|
|
281
|
+
skill: catalog.find((skill) => skill.path === path)?.name,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Two failed checks and the agent must stop offering the script.
|
|
291
|
+
*
|
|
292
|
+
* Counted here rather than trusted to the prompt: after the budget is spent the
|
|
293
|
+
* tool itself refuses, so a model that keeps trying is told no by the runtime.
|
|
294
|
+
*/
|
|
295
|
+
const MAX_FAILED_CHECKS = 2;
|
|
296
|
+
/**
|
|
297
|
+
* The tool a draft script must pass before the model shows it. It counts failed
|
|
298
|
+
* checks itself, so the limit holds whether or not the model follows the prompt.
|
|
299
|
+
* A check that passes does not count.
|
|
300
|
+
*/
|
|
301
|
+
function createCheckTool(stone, attempts) {
|
|
302
|
+
const say = (text, isError = false) => ({
|
|
303
|
+
content: [{ type: "text", text }],
|
|
304
|
+
details: {},
|
|
305
|
+
isError,
|
|
306
|
+
});
|
|
307
|
+
return defineTool({
|
|
308
|
+
name: CHECK_TOOL,
|
|
309
|
+
label: "Check script",
|
|
310
|
+
description: "Check a draft script against this language's checkers before showing it to " +
|
|
311
|
+
"anyone. Returns whether it passed and, when it did not, the checker's " +
|
|
312
|
+
"diagnostics. Always call this before putting a script in your reply.",
|
|
313
|
+
parameters: Type.Object({
|
|
314
|
+
script: Type.String({
|
|
315
|
+
description: "The complete draft script to check.",
|
|
316
|
+
}),
|
|
317
|
+
}),
|
|
318
|
+
// One check at a time, so each one sees the failures counted before it.
|
|
319
|
+
executionMode: "sequential",
|
|
320
|
+
async execute(_id, { script }, signal) {
|
|
321
|
+
if (attempts.failed >= MAX_FAILED_CHECKS) {
|
|
322
|
+
return say("Attempt limit reached. Do not show this script. Tell the person what the " +
|
|
323
|
+
"checker rejected and what you would need to get it right.", true);
|
|
324
|
+
}
|
|
325
|
+
const report = await runChecks(stone.checks, script, stone.path, signal);
|
|
326
|
+
if (report.ok)
|
|
327
|
+
return say("All checks passed. The script is safe to show.");
|
|
328
|
+
attempts.failed += 1;
|
|
329
|
+
const failedChecks = report.checks.filter((check) => !check.ok);
|
|
330
|
+
const remaining = MAX_FAILED_CHECKS - attempts.failed;
|
|
331
|
+
return say([
|
|
332
|
+
...failedChecks.map((check) => `${check.name} failed:\n${check.output || "(no output)"}`),
|
|
333
|
+
remaining > 0
|
|
334
|
+
? `Fix the script and check again. ${remaining} attempt left.`
|
|
335
|
+
: "No attempts left. Do not show this script; explain what failed.",
|
|
336
|
+
].join("\n\n"), true);
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Rosetta resolves the model itself rather than letting pi pick silently.
|
|
342
|
+
*
|
|
343
|
+
* pi's own order is session, then settings.defaultModel, then first available.
|
|
344
|
+
* That is right for a terminal where you can see and change the model, but on a
|
|
345
|
+
* shared server the admin needs to know which model is answering, so we resolve
|
|
346
|
+
* explicitly and report it at startup.
|
|
347
|
+
*/
|
|
348
|
+
async function resolveModel(modelRuntime, pattern) {
|
|
349
|
+
// 1. ROSETTA_MODEL wins. Accepts pi's own syntax, including "provider/id"
|
|
350
|
+
// and a ":thinking" suffix, e.g. "anthropic/claude-sonnet-5:high".
|
|
351
|
+
if (pattern) {
|
|
352
|
+
const result = resolveCliModel({ cliModel: pattern, modelRuntime });
|
|
353
|
+
if (result.error)
|
|
354
|
+
throw new Error(`ROSETTA_MODEL: ${result.error}`);
|
|
355
|
+
if (result.warning)
|
|
356
|
+
console.warn(`ROSETTA_MODEL: ${result.warning}`);
|
|
357
|
+
return {
|
|
358
|
+
model: result.model,
|
|
359
|
+
thinkingLevel: result.thinkingLevel,
|
|
360
|
+
source: "ROSETTA_MODEL",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// 2. Whatever the admin set as pi's default on this host.
|
|
364
|
+
try {
|
|
365
|
+
const settingsPath = resolvePath(getAgentDir(), "settings.json");
|
|
366
|
+
const settings = JSON.parse(await readFile(settingsPath, "utf8"));
|
|
367
|
+
if (typeof settings.defaultModel === "string" && settings.defaultModel) {
|
|
368
|
+
const result = resolveCliModel({
|
|
369
|
+
cliModel: typeof settings.defaultProvider === "string"
|
|
370
|
+
? `${settings.defaultProvider}/${settings.defaultModel}`
|
|
371
|
+
: settings.defaultModel,
|
|
372
|
+
modelRuntime,
|
|
373
|
+
});
|
|
374
|
+
if (result.model) {
|
|
375
|
+
return {
|
|
376
|
+
model: result.model,
|
|
377
|
+
thinkingLevel: result.thinkingLevel,
|
|
378
|
+
source: "pi defaultModel",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// No settings file, or unreadable. Fall through.
|
|
385
|
+
}
|
|
386
|
+
// 3. Anything with working credentials.
|
|
387
|
+
const available = await modelRuntime.getAvailable();
|
|
388
|
+
if (!available.length) {
|
|
389
|
+
throw new Error("No model is available. Run `pi` and `/login`, or set provider API keys, then restart.");
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
model: available[0],
|
|
393
|
+
thinkingLevel: undefined,
|
|
394
|
+
source: "first available",
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** Explicit resources: no host prompt, parent AGENTS, packages, extensions, or global skills discovery. */
|
|
398
|
+
export function createStoneLoader(stone) {
|
|
399
|
+
const root = realpathSync(stone.path);
|
|
400
|
+
const loaded = loadSkillsFromDir({
|
|
401
|
+
dir: join(stone.path, "skills"),
|
|
402
|
+
source: "project",
|
|
403
|
+
});
|
|
404
|
+
const skills = loaded.skills.filter((skill) => {
|
|
405
|
+
try {
|
|
406
|
+
return contains(root, realpathSync(skill.filePath));
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
const extensions = {
|
|
413
|
+
extensions: [],
|
|
414
|
+
errors: [],
|
|
415
|
+
runtime: createExtensionRuntime(),
|
|
416
|
+
};
|
|
417
|
+
return {
|
|
418
|
+
getExtensions: () => extensions,
|
|
419
|
+
getSkills: () => ({ skills, diagnostics: loaded.diagnostics }),
|
|
420
|
+
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
421
|
+
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
422
|
+
getAgentsFiles: () => ({
|
|
423
|
+
agentsFiles: stone.context.map((file) => ({
|
|
424
|
+
path: join(stone.path, file.path),
|
|
425
|
+
content: file.content,
|
|
426
|
+
})),
|
|
427
|
+
}),
|
|
428
|
+
getSystemPrompt: () => stone.systemPrompt,
|
|
429
|
+
getSystemPromptSource: () => undefined,
|
|
430
|
+
getAppendSystemPrompt: () => [],
|
|
431
|
+
getAppendSystemPromptSources: () => [],
|
|
432
|
+
extendResources: () => { },
|
|
433
|
+
reload: async () => { }, // Resources remain fixed for the lifetime of the process.
|
|
434
|
+
};
|
|
435
|
+
}
|