nolo-cli 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -44,6 +44,8 @@ export async function runLocalAgentTurn(
|
|
|
44
44
|
const history = input.continueDialogId
|
|
45
45
|
? await input.adapter.loadDialogHistory(input.continueDialogId)
|
|
46
46
|
: [];
|
|
47
|
+
const promptMessageCount = agentConfig.prompt?.trim() ? 1 : 0;
|
|
48
|
+
const turnStartIndex = promptMessageCount + history.length;
|
|
47
49
|
const messages = buildMessages({
|
|
48
50
|
prompt: agentConfig.prompt,
|
|
49
51
|
history,
|
|
@@ -76,17 +78,23 @@ export async function runLocalAgentTurn(
|
|
|
76
78
|
role: "tool",
|
|
77
79
|
content: toolResult.content,
|
|
78
80
|
tool_call_id: toolCall.id,
|
|
81
|
+
...(toolResult.metadata ? { tool_result_metadata: toolResult.metadata } : {}),
|
|
79
82
|
});
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
result = result!;
|
|
86
|
+
messages.push({
|
|
87
|
+
role: "assistant",
|
|
88
|
+
content: result.content,
|
|
89
|
+
});
|
|
83
90
|
const saved = await input.adapter.saveTurn({
|
|
84
91
|
agentKey: agentConfig.key,
|
|
85
|
-
messages,
|
|
92
|
+
messages: messages.slice(turnStartIndex),
|
|
86
93
|
result: {
|
|
87
94
|
...result,
|
|
88
95
|
...(toolCallCount > 0 ? { toolCallCount } : {}),
|
|
89
96
|
},
|
|
97
|
+
...(input.continueDialogId ? { continueDialogId: input.continueDialogId } : {}),
|
|
90
98
|
});
|
|
91
99
|
|
|
92
100
|
return {
|
package/agent-runtime/types.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface AgentRuntimeChatMessage {
|
|
|
33
33
|
content: AgentRuntimeMessageContent;
|
|
34
34
|
tool_call_id?: string;
|
|
35
35
|
tool_calls?: AgentRuntimeToolCall[];
|
|
36
|
+
tool_result_metadata?: Record<string, unknown>;
|
|
36
37
|
reasoning_content?: string;
|
|
37
38
|
cybotKey?: string;
|
|
38
39
|
agentKey?: string;
|
package/agentRunCommand.ts
CHANGED
|
@@ -181,10 +181,19 @@ export async function runAgentRunCommand(args: string[], deps: AgentRunCommandDe
|
|
|
181
181
|
const runner = deps.runner ?? runAgentTurn;
|
|
182
182
|
let localRuntimeCwd: string | undefined;
|
|
183
183
|
if (parsed.allowShell) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
try {
|
|
185
|
+
localRuntimeCwd = await (deps.prepareShellWorktree ?? prepareShellWorktree)({
|
|
186
|
+
agentKey: parsed.agentKey,
|
|
187
|
+
env,
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
output.write(
|
|
191
|
+
"[nolo] Could not prepare a local shell worktree.\n" +
|
|
192
|
+
"Run from inside a git repository, or set NOLO_LOCAL_WORKTREE to an existing isolated checkout.\n" +
|
|
193
|
+
`Reason: ${error instanceof Error ? error.message : String(error)}\n`
|
|
194
|
+
);
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
188
197
|
}
|
|
189
198
|
const runEnv = parsed.allowShell
|
|
190
199
|
? {
|
|
@@ -81,7 +81,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
81
81
|
expect(result).toMatchObject({
|
|
82
82
|
content: "local adapter ok",
|
|
83
83
|
model: "gpt-4.1-mini",
|
|
84
|
-
dialogId: "
|
|
84
|
+
dialogId: "dialog-existing",
|
|
85
85
|
});
|
|
86
86
|
expect(requests).toEqual([{
|
|
87
87
|
url: "http://127.0.0.1:11434/v1/chat/completions",
|
|
@@ -97,29 +97,210 @@ describe("CLI local runtime adapter", () => {
|
|
|
97
97
|
},
|
|
98
98
|
}]);
|
|
99
99
|
expect(batchOps.map((op) => op.key)).toEqual([
|
|
100
|
-
"dialog-user-1-
|
|
101
|
-
"dialog-
|
|
102
|
-
"dialog-
|
|
100
|
+
"dialog-user-1-dialog-existing",
|
|
101
|
+
"dialog-dialog-existing-msg-1710000000000-001",
|
|
102
|
+
"dialog-dialog-existing-msg-1710000000000-002",
|
|
103
103
|
]);
|
|
104
|
-
expect(store.get("dialog-user-1-
|
|
105
|
-
id: "
|
|
106
|
-
dbKey: "dialog-user-1-
|
|
104
|
+
expect(store.get("dialog-user-1-dialog-existing")).toMatchObject({
|
|
105
|
+
id: "dialog-existing",
|
|
106
|
+
dbKey: "dialog-user-1-dialog-existing",
|
|
107
107
|
type: "dialog",
|
|
108
108
|
primaryAgentKey: "agent-user-1-frontend",
|
|
109
109
|
status: "done",
|
|
110
110
|
});
|
|
111
|
-
expect(store.get("dialog-
|
|
112
|
-
dialogId: "
|
|
111
|
+
expect(store.get("dialog-dialog-existing-msg-1710000000000-001")).toMatchObject({
|
|
112
|
+
dialogId: "dialog-existing",
|
|
113
113
|
role: "user",
|
|
114
114
|
content: "make it cleaner",
|
|
115
115
|
});
|
|
116
|
-
expect(store.get("dialog-
|
|
117
|
-
dialogId: "
|
|
116
|
+
expect(store.get("dialog-dialog-existing-msg-1710000000000-002")).toMatchObject({
|
|
117
|
+
dialogId: "dialog-existing",
|
|
118
118
|
role: "assistant",
|
|
119
119
|
content: "local adapter ok",
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
test("saves local tool call trace and shell metadata into the local dialog", async () => {
|
|
124
|
+
const store = new Map<string, any>([
|
|
125
|
+
["agent-user-1-shell", {
|
|
126
|
+
dbKey: "agent-user-1-shell",
|
|
127
|
+
id: "shell",
|
|
128
|
+
prompt: "Use shell.",
|
|
129
|
+
model: "gpt-4.1-mini",
|
|
130
|
+
}],
|
|
131
|
+
]);
|
|
132
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
133
|
+
env: {
|
|
134
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
135
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
136
|
+
NOLO_LOCAL_SHELL_MODE: "worktree",
|
|
137
|
+
},
|
|
138
|
+
db: {
|
|
139
|
+
get: async (key) => {
|
|
140
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
141
|
+
return store.get(key);
|
|
142
|
+
},
|
|
143
|
+
put: async (key, value) => {
|
|
144
|
+
store.set(key, value);
|
|
145
|
+
},
|
|
146
|
+
batch: async (ops) => {
|
|
147
|
+
for (const op of ops) {
|
|
148
|
+
if (op.type === "put") store.set(op.key, op.value);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
iterator: () => (async function* () {})(),
|
|
152
|
+
},
|
|
153
|
+
cwd: import.meta.dir,
|
|
154
|
+
now: () => 1710000000000,
|
|
155
|
+
createId: () => "01TRACE",
|
|
156
|
+
fetchImpl: async (_url, init) => {
|
|
157
|
+
const body = JSON.parse(String(init?.body));
|
|
158
|
+
const hasToolResult = body.messages.some((message: any) => message.role === "tool");
|
|
159
|
+
if (!hasToolResult) {
|
|
160
|
+
return Response.json({
|
|
161
|
+
choices: [{
|
|
162
|
+
message: {
|
|
163
|
+
content: "",
|
|
164
|
+
tool_calls: [{
|
|
165
|
+
id: "call-shell",
|
|
166
|
+
type: "function",
|
|
167
|
+
function: {
|
|
168
|
+
name: "execShell",
|
|
169
|
+
arguments: JSON.stringify({ cmd: "printf trace-ok" }),
|
|
170
|
+
},
|
|
171
|
+
}],
|
|
172
|
+
},
|
|
173
|
+
}],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return Response.json({
|
|
177
|
+
choices: [{ message: { content: "done" } }],
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const result = await runLocalAgentTurn({
|
|
183
|
+
adapter,
|
|
184
|
+
agentRef: "shell",
|
|
185
|
+
input: "inspect",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result.dialogId).toBe("01TRACE");
|
|
189
|
+
expect(store.get("dialog-user-1-01TRACE")).toMatchObject({
|
|
190
|
+
toolCallCount: 1,
|
|
191
|
+
localRuntime: expect.objectContaining({
|
|
192
|
+
host: "cli",
|
|
193
|
+
worktreePath: import.meta.dir,
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
const messages = [...store.entries()]
|
|
197
|
+
.filter(([key]) => key.startsWith("dialog-01TRACE-msg-"))
|
|
198
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
199
|
+
.map(([, value]) => value);
|
|
200
|
+
expect(messages.map((message) => message.role)).toEqual([
|
|
201
|
+
"user",
|
|
202
|
+
"assistant",
|
|
203
|
+
"tool",
|
|
204
|
+
"assistant",
|
|
205
|
+
]);
|
|
206
|
+
expect(messages[1]).toMatchObject({
|
|
207
|
+
tool_calls: [{
|
|
208
|
+
id: "call-shell",
|
|
209
|
+
function: { name: "execShell" },
|
|
210
|
+
}],
|
|
211
|
+
});
|
|
212
|
+
expect(messages[2]).toMatchObject({
|
|
213
|
+
role: "tool",
|
|
214
|
+
toolCallId: "call-shell",
|
|
215
|
+
metadata: { exitCode: 0 },
|
|
216
|
+
});
|
|
217
|
+
expect(messages[2].content).toContain("trace-ok");
|
|
218
|
+
expect(messages[3]).toMatchObject({
|
|
219
|
+
role: "assistant",
|
|
220
|
+
content: "done",
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("continues a local dialog instead of creating a new one", async () => {
|
|
225
|
+
const store = new Map<string, any>([
|
|
226
|
+
["agent-user-1-frontend", {
|
|
227
|
+
dbKey: "agent-user-1-frontend",
|
|
228
|
+
id: "frontend",
|
|
229
|
+
prompt: "Fix UI",
|
|
230
|
+
model: "gpt-4.1-mini",
|
|
231
|
+
}],
|
|
232
|
+
["dialog-user-1-dialog-existing", {
|
|
233
|
+
dbKey: "dialog-user-1-dialog-existing",
|
|
234
|
+
id: "dialog-existing",
|
|
235
|
+
type: "dialog",
|
|
236
|
+
userId: "user-1",
|
|
237
|
+
title: "Existing dialog",
|
|
238
|
+
}],
|
|
239
|
+
["dialog-dialog-existing-msg-001", {
|
|
240
|
+
dbKey: "dialog-dialog-existing-msg-001",
|
|
241
|
+
id: "msg-001",
|
|
242
|
+
dialogId: "dialog-existing",
|
|
243
|
+
role: "assistant",
|
|
244
|
+
content: "previous answer",
|
|
245
|
+
}],
|
|
246
|
+
]);
|
|
247
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
248
|
+
env: {
|
|
249
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
250
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
251
|
+
},
|
|
252
|
+
db: {
|
|
253
|
+
get: async (key) => {
|
|
254
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
255
|
+
return store.get(key);
|
|
256
|
+
},
|
|
257
|
+
put: async (key, value) => {
|
|
258
|
+
store.set(key, value);
|
|
259
|
+
},
|
|
260
|
+
batch: async (ops) => {
|
|
261
|
+
for (const op of ops) {
|
|
262
|
+
if (op.type === "put") store.set(op.key, op.value);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
iterator: ({ gte, lte }) => (async function* () {
|
|
266
|
+
for (const entry of [...store.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
267
|
+
if (entry[0] >= gte && entry[0] <= lte) yield entry;
|
|
268
|
+
}
|
|
269
|
+
})(),
|
|
270
|
+
},
|
|
271
|
+
now: () => 1710000000000,
|
|
272
|
+
createId: () => "SHOULDNOTUSE",
|
|
273
|
+
fetchImpl: async (_url, init) => {
|
|
274
|
+
const body = JSON.parse(String(init?.body));
|
|
275
|
+
expect(body.messages).toContainEqual({
|
|
276
|
+
role: "assistant",
|
|
277
|
+
content: "previous answer",
|
|
278
|
+
});
|
|
279
|
+
return Response.json({
|
|
280
|
+
choices: [{ message: { content: "continued" } }],
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const result = await runLocalAgentTurn({
|
|
286
|
+
adapter,
|
|
287
|
+
agentRef: "frontend",
|
|
288
|
+
input: "continue",
|
|
289
|
+
continueDialogId: "dialog-existing",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(result.dialogId).toBe("dialog-existing");
|
|
293
|
+
expect(store.has("dialog-user-1-SHOULDNOTUSE")).toBe(false);
|
|
294
|
+
expect(store.get("dialog-user-1-dialog-existing")).toMatchObject({
|
|
295
|
+
id: "dialog-existing",
|
|
296
|
+
title: "Existing dialog",
|
|
297
|
+
status: "done",
|
|
298
|
+
});
|
|
299
|
+
expect([...store.keys()].filter((key) => key.startsWith("dialog-dialog-existing-msg-"))).toContain(
|
|
300
|
+
"dialog-dialog-existing-msg-1710000000000-001"
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
123
304
|
test("passes image_url message parts through to OpenAI-compatible providers", async () => {
|
|
124
305
|
const requests: Array<{ body: any }> = [];
|
|
125
306
|
const adapter = createCliLocalRuntimeAdapter({
|
|
@@ -161,6 +161,8 @@ function toOpenAiMessages(messages: AgentRuntimeChatMessage[]) {
|
|
|
161
161
|
return messages.map((message) => ({
|
|
162
162
|
role: message.role,
|
|
163
163
|
content: message.content ?? "",
|
|
164
|
+
...(message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}),
|
|
165
|
+
...(Array.isArray(message.tool_calls) ? { tool_calls: message.tool_calls } : {}),
|
|
164
166
|
}));
|
|
165
167
|
}
|
|
166
168
|
|
|
@@ -192,8 +194,9 @@ async function writeDialog(args: {
|
|
|
192
194
|
userId: string;
|
|
193
195
|
now: () => number;
|
|
194
196
|
createId: () => string;
|
|
197
|
+
cwd?: string;
|
|
195
198
|
}) {
|
|
196
|
-
const dialogId = args.createId();
|
|
199
|
+
const dialogId = args.input.continueDialogId || args.createId();
|
|
197
200
|
const now = args.now();
|
|
198
201
|
const nowIso = new Date(now).toISOString();
|
|
199
202
|
const dialogKey = `dialog-${args.userId}-${dialogId}`;
|
|
@@ -206,56 +209,74 @@ async function writeDialog(args: {
|
|
|
206
209
|
.map((part) => part.text)
|
|
207
210
|
.join(" ")
|
|
208
211
|
: "";
|
|
212
|
+
let existingDialog: any = null;
|
|
213
|
+
if (args.input.continueDialogId) {
|
|
214
|
+
try {
|
|
215
|
+
existingDialog = await args.db.get(dialogKey);
|
|
216
|
+
} catch {
|
|
217
|
+
existingDialog = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const messageOps = args.input.messages
|
|
221
|
+
.filter((message) => message.role !== "system")
|
|
222
|
+
.map((message, index) => {
|
|
223
|
+
const id = `${now}-${String(index + 1).padStart(3, "0")}`;
|
|
224
|
+
const key = `dialog-${dialogId}-msg-${id}`;
|
|
225
|
+
return {
|
|
226
|
+
type: "put" as const,
|
|
227
|
+
key,
|
|
228
|
+
value: {
|
|
229
|
+
id,
|
|
230
|
+
dbKey: key,
|
|
231
|
+
dialogId,
|
|
232
|
+
role: message.role,
|
|
233
|
+
content: message.content ?? "",
|
|
234
|
+
...(message.role === "user" ? { userId: args.userId } : {}),
|
|
235
|
+
...(message.role === "assistant" ? {
|
|
236
|
+
agentKey: args.input.agentKey,
|
|
237
|
+
cybotKey: args.input.agentKey,
|
|
238
|
+
} : {}),
|
|
239
|
+
...(message.tool_call_id ? { toolCallId: message.tool_call_id } : {}),
|
|
240
|
+
...(Array.isArray(message.tool_calls) ? { tool_calls: message.tool_calls } : {}),
|
|
241
|
+
...(message.tool_result_metadata ? { metadata: message.tool_result_metadata } : {}),
|
|
242
|
+
createdAt: nowIso,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
});
|
|
209
246
|
const ops: Array<{ type: "put"; key: string; value: any }> = [
|
|
210
247
|
{
|
|
211
248
|
type: "put",
|
|
212
249
|
key: dialogKey,
|
|
213
250
|
value: {
|
|
251
|
+
...(existingDialog && typeof existingDialog === "object" ? existingDialog : {}),
|
|
214
252
|
id: dialogId,
|
|
215
253
|
dbKey: dialogKey,
|
|
216
254
|
type: "dialog",
|
|
217
255
|
userId: args.userId,
|
|
218
256
|
cybots: [args.input.agentKey],
|
|
219
257
|
primaryAgentKey: args.input.agentKey,
|
|
220
|
-
title:
|
|
221
|
-
?
|
|
222
|
-
:
|
|
258
|
+
title: typeof existingDialog?.title === "string" && existingDialog.title.trim()
|
|
259
|
+
? existingDialog.title
|
|
260
|
+
: lastUserText.trim()
|
|
261
|
+
? lastUserText.trim().slice(0, 80)
|
|
262
|
+
: "Local agent run",
|
|
223
263
|
status: "done",
|
|
224
264
|
triggerType: "cli-local",
|
|
225
265
|
executionMode: "foreground",
|
|
226
|
-
createdAt: nowIso,
|
|
266
|
+
createdAt: existingDialog?.createdAt ?? nowIso,
|
|
227
267
|
updatedAt: nowIso,
|
|
228
268
|
finishedAt: now,
|
|
229
269
|
usage: args.input.result.usage,
|
|
270
|
+
...(typeof args.input.result.toolCallCount === "number"
|
|
271
|
+
? { toolCallCount: args.input.result.toolCallCount }
|
|
272
|
+
: {}),
|
|
273
|
+
localRuntime: {
|
|
274
|
+
host: "cli",
|
|
275
|
+
...(args.cwd ? { worktreePath: args.cwd } : {}),
|
|
276
|
+
},
|
|
230
277
|
},
|
|
231
278
|
},
|
|
232
|
-
|
|
233
|
-
type: "put",
|
|
234
|
-
key: `dialog-${dialogId}-msg-user`,
|
|
235
|
-
value: {
|
|
236
|
-
id: "msg-user",
|
|
237
|
-
dbKey: `dialog-${dialogId}-msg-user`,
|
|
238
|
-
dialogId,
|
|
239
|
-
role: "user",
|
|
240
|
-
content: lastUser?.content ?? "",
|
|
241
|
-
userId: args.userId,
|
|
242
|
-
createdAt: nowIso,
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
type: "put",
|
|
247
|
-
key: `dialog-${dialogId}-msg-assistant`,
|
|
248
|
-
value: {
|
|
249
|
-
id: "msg-assistant",
|
|
250
|
-
dbKey: `dialog-${dialogId}-msg-assistant`,
|
|
251
|
-
dialogId,
|
|
252
|
-
role: "assistant",
|
|
253
|
-
content: args.input.result.content,
|
|
254
|
-
agentKey: args.input.agentKey,
|
|
255
|
-
cybotKey: args.input.agentKey,
|
|
256
|
-
createdAt: nowIso,
|
|
257
|
-
},
|
|
258
|
-
},
|
|
279
|
+
...messageOps,
|
|
259
280
|
];
|
|
260
281
|
await args.db.batch(ops);
|
|
261
282
|
return { dialogId };
|
|
@@ -293,6 +314,7 @@ export function createCliLocalRuntimeAdapter(
|
|
|
293
314
|
userId,
|
|
294
315
|
now,
|
|
295
316
|
createId,
|
|
317
|
+
cwd: deps.cwd,
|
|
296
318
|
}),
|
|
297
319
|
resolveProvider: async (agentConfig) => ({
|
|
298
320
|
model: agentConfig.model || "gpt-4.1-mini",
|