dsh-plugin-jules 0.1.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/LICENSE +21 -0
- package/README.md +335 -0
- package/cordis.patch.yml +37 -0
- package/lib/async.d.ts +11 -0
- package/lib/async.js +27 -0
- package/lib/client.d.ts +207 -0
- package/lib/client.js +368 -0
- package/lib/index.d.ts +107 -0
- package/lib/index.js +234 -0
- package/lib/journal.d.ts +78 -0
- package/lib/journal.js +110 -0
- package/lib/tools.d.ts +43 -0
- package/lib/tools.js +576 -0
- package/lib/types.d.ts +230 -0
- package/lib/types.js +128 -0
- package/lib/views.d.ts +240 -0
- package/lib/views.js +438 -0
- package/lib/watch.d.ts +87 -0
- package/lib/watch.js +387 -0
- package/package.json +86 -0
- package/scripts/build.mjs +37 -0
- package/scripts/link-dsh-deps.mjs +132 -0
package/lib/tools.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-facing `jules_*` tool family.
|
|
3
|
+
*
|
|
4
|
+
* Every tool returns a canonical value described by its own output schema and
|
|
5
|
+
* renders that value to text separately, so a caller that needs an id or a
|
|
6
|
+
* state never has to parse the prose.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-plugin-jules/tools
|
|
9
|
+
*/
|
|
10
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
11
|
+
import { sleep } from "./async.js";
|
|
12
|
+
import { DEFAULT_WAIT_STATES, sessionIdOf, sourceNameOf } from "./types.js";
|
|
13
|
+
import { activityRow, latestPatch, patchFiles, renderActivities, renderPatch, renderSessionDetail, renderSessionRows, renderSources, renderWait, sessionDetail, sessionRow, sourceRow, } from "./views.js";
|
|
14
|
+
import { runWatch } from "./watch.js";
|
|
15
|
+
/** Attempts allowed for an approval to become visible in the log. */
|
|
16
|
+
const APPROVAL_CONFIRM_ATTEMPTS = 3;
|
|
17
|
+
/**
|
|
18
|
+
* First delay between approval-confirmation reads, in milliseconds. Short
|
|
19
|
+
* because the lag is normally sub-second; the total window is the interesting
|
|
20
|
+
* number, and it stays inside a tool call's patience.
|
|
21
|
+
*/
|
|
22
|
+
const APPROVAL_CONFIRM_DELAY_MS = 750;
|
|
23
|
+
/**
|
|
24
|
+
* Whether one activity is the plan-approval event.
|
|
25
|
+
* @param activity - one activity from the log.
|
|
26
|
+
* @returns whether the plan was approved.
|
|
27
|
+
*/
|
|
28
|
+
function isPlanApproved(activity) {
|
|
29
|
+
return activity.planApproved !== undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The sentence that leads an approval reply.
|
|
33
|
+
*
|
|
34
|
+
* Three outcomes, and they are not interchangeable: confirmed, sent with the
|
|
35
|
+
* event not yet visible, and sent with the log unreadable. Reporting the third
|
|
36
|
+
* as the second would read as a failed approval when nothing is known either way.
|
|
37
|
+
* @param id - session id.
|
|
38
|
+
* @param confirmed - whether the planApproved event was observed.
|
|
39
|
+
* @param logRead - whether the log could be read at all.
|
|
40
|
+
* @returns the leading sentence.
|
|
41
|
+
*/
|
|
42
|
+
function approvalLead(id, confirmed, logRead) {
|
|
43
|
+
if (confirmed)
|
|
44
|
+
return `Approved the plan for session ${id}; the planApproved event is in the log.`;
|
|
45
|
+
if (!logRead) {
|
|
46
|
+
return `Approval sent for session ${id}, but the activity log could not be read, so it could not be verified `
|
|
47
|
+
+ 'either way. Check jules_activities rather than assuming it failed.';
|
|
48
|
+
}
|
|
49
|
+
return `Approval sent for session ${id}, but its planApproved event had not appeared after `
|
|
50
|
+
+ APPROVAL_CONFIRM_ATTEMPTS + ' reads. That is the service lagging, not a failed approval — '
|
|
51
|
+
+ 'confirm with jules_activities before assuming anything.';
|
|
52
|
+
}
|
|
53
|
+
/** One progress row shared by the session and wait projections. */
|
|
54
|
+
const GENERATED_FILE_SCHEMA = {
|
|
55
|
+
type: 'object', additionalProperties: false,
|
|
56
|
+
properties: {
|
|
57
|
+
path: { type: 'string', required: true },
|
|
58
|
+
changeType: { type: 'string', required: true },
|
|
59
|
+
bytes: { type: 'integer', required: true },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
/** The full session projection returned by create, status, approve, and message. */
|
|
63
|
+
const SESSION_DETAIL_SCHEMA = {
|
|
64
|
+
type: 'object', additionalProperties: false,
|
|
65
|
+
properties: {
|
|
66
|
+
id: { type: 'string', required: true },
|
|
67
|
+
title: { type: 'string', required: true },
|
|
68
|
+
state: { type: 'string', required: true },
|
|
69
|
+
url: { type: 'string', required: true },
|
|
70
|
+
prompt: { type: 'string', required: true },
|
|
71
|
+
source: { type: 'string', required: true },
|
|
72
|
+
branch: { type: 'string', required: true },
|
|
73
|
+
requirePlanApproval: { type: 'string', required: true, description: 'yes | no | unknown — the service does not echo this create-time input, so unknown beats a fabricated no.' },
|
|
74
|
+
autoCreatePr: { type: 'string', required: true },
|
|
75
|
+
archived: { type: 'boolean', required: true },
|
|
76
|
+
createTime: { type: 'string', required: true },
|
|
77
|
+
updateTime: { type: 'string', required: true },
|
|
78
|
+
planId: { type: 'string', required: true },
|
|
79
|
+
planSteps: { type: 'array', required: true, items: { type: 'string' } },
|
|
80
|
+
planPending: { type: 'boolean', required: true },
|
|
81
|
+
planApproved: { type: 'boolean', required: true },
|
|
82
|
+
pullRequests: { type: 'array', required: true, items: { type: 'string' } },
|
|
83
|
+
generatedFiles: { type: 'array', required: true, items: GENERATED_FILE_SCHEMA },
|
|
84
|
+
latestCommand: { type: 'string', required: true },
|
|
85
|
+
lastMessage: { type: 'string', required: true },
|
|
86
|
+
lastProgress: { type: 'string', required: true },
|
|
87
|
+
unchangedForMs: { type: 'integer', required: true },
|
|
88
|
+
approvalConfirmed: { type: 'boolean', required: true },
|
|
89
|
+
logRead: { type: 'boolean', required: true },
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
/** One session row in the `jules_list` projection. */
|
|
93
|
+
const SESSION_ROW_SCHEMA = {
|
|
94
|
+
type: 'object', additionalProperties: false,
|
|
95
|
+
properties: {
|
|
96
|
+
id: { type: 'string', required: true },
|
|
97
|
+
title: { type: 'string', required: true },
|
|
98
|
+
state: { type: 'string', required: true },
|
|
99
|
+
url: { type: 'string', required: true },
|
|
100
|
+
source: { type: 'string', required: true },
|
|
101
|
+
branch: { type: 'string', required: true },
|
|
102
|
+
createTime: { type: 'string', required: true },
|
|
103
|
+
updateTime: { type: 'string', required: true },
|
|
104
|
+
pullRequests: { type: 'array', required: true, items: { type: 'string' } },
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
/** The `jules_sources` projection. */
|
|
108
|
+
const SOURCE_SCHEMA = {
|
|
109
|
+
type: 'object', additionalProperties: false,
|
|
110
|
+
properties: {
|
|
111
|
+
name: { type: 'string', required: true },
|
|
112
|
+
owner: { type: 'string', required: true },
|
|
113
|
+
repo: { type: 'string', required: true },
|
|
114
|
+
isPrivate: { type: 'boolean', required: true },
|
|
115
|
+
defaultBranch: { type: 'string', required: true },
|
|
116
|
+
branches: { type: 'array', required: true, items: { type: 'string' } },
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
/** One activity row in the `jules_activities` projection. */
|
|
120
|
+
const ACTIVITY_SCHEMA = {
|
|
121
|
+
type: 'object', additionalProperties: false,
|
|
122
|
+
properties: {
|
|
123
|
+
id: { type: 'string', required: true },
|
|
124
|
+
time: { type: 'string', required: true },
|
|
125
|
+
originator: { type: 'string', required: true },
|
|
126
|
+
kind: { type: 'string', required: true },
|
|
127
|
+
summary: { type: 'string', required: true },
|
|
128
|
+
artifacts: { type: 'array', required: true, items: { type: 'string' } },
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
/** The `jules_wait` projection. */
|
|
132
|
+
const WAIT_SCHEMA = {
|
|
133
|
+
type: 'object', additionalProperties: false,
|
|
134
|
+
properties: {
|
|
135
|
+
id: { type: 'string', required: true },
|
|
136
|
+
title: { type: 'string', required: true },
|
|
137
|
+
state: { type: 'string', required: true },
|
|
138
|
+
url: { type: 'string', required: true },
|
|
139
|
+
settled: { type: 'boolean', required: true },
|
|
140
|
+
timedOut: { type: 'boolean', required: true },
|
|
141
|
+
needsAttention: { type: 'boolean', required: true },
|
|
142
|
+
waitedMs: { type: 'integer', required: true },
|
|
143
|
+
polls: { type: 'integer', required: true },
|
|
144
|
+
planId: { type: 'string', required: true },
|
|
145
|
+
planSteps: { type: 'array', required: true, items: { type: 'string' } },
|
|
146
|
+
pullRequests: { type: 'array', required: true, items: { type: 'string' } },
|
|
147
|
+
lastMessage: { type: 'string', required: true },
|
|
148
|
+
lastProgress: { type: 'string', required: true },
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
/** The `jules_patch` projection. */
|
|
152
|
+
const PATCH_SCHEMA = {
|
|
153
|
+
type: 'object', additionalProperties: false,
|
|
154
|
+
properties: {
|
|
155
|
+
id: { type: 'string', required: true },
|
|
156
|
+
found: { type: 'boolean', required: true },
|
|
157
|
+
patch: { type: 'string', required: true },
|
|
158
|
+
bytes: { type: 'integer', required: true },
|
|
159
|
+
truncated: { type: 'boolean', required: true },
|
|
160
|
+
offset: { type: 'integer', required: true },
|
|
161
|
+
nextOffset: { type: 'integer', required: true },
|
|
162
|
+
files: { type: 'array', required: true, items: { type: 'string' } },
|
|
163
|
+
baseCommitId: { type: 'string', required: true },
|
|
164
|
+
suggestedCommitMessage: { type: 'string', required: true },
|
|
165
|
+
source: { type: 'string', required: true },
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Read a session and its event log, tolerating a log that cannot be read yet.
|
|
170
|
+
*
|
|
171
|
+
* A session created moments ago can exist before its first activity does, so a
|
|
172
|
+
* failed read degrades the projection rather than failing the call.
|
|
173
|
+
* @param client - the Jules client.
|
|
174
|
+
* @param id - bare session id.
|
|
175
|
+
* @param signal - the call's cancellation signal.
|
|
176
|
+
* @param maxPages - activity page cap.
|
|
177
|
+
* @returns the session, whatever activities were readable, and whether the read worked.
|
|
178
|
+
*/
|
|
179
|
+
async function readSessionDetail(client, id, signal, maxPages) {
|
|
180
|
+
const session = await client.getSession(id, signal);
|
|
181
|
+
try {
|
|
182
|
+
return { session, activities: await client.listAllActivities(id, { signal, maxPages, pageSize: 100 }), logRead: true };
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
// The session itself was readable, so the call still has something to
|
|
186
|
+
// report — but an unreadable log must never masquerade as an empty one.
|
|
187
|
+
// `logRead` is what keeps "no plan pending" from meaning "we could not look".
|
|
188
|
+
if (signal.aborted)
|
|
189
|
+
throw error;
|
|
190
|
+
return { session, activities: [], logRead: false };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Register every `jules_*` tool.
|
|
195
|
+
*
|
|
196
|
+
* Concurrency classification is a rule, not a per-tool judgement: every
|
|
197
|
+
* read-only tool declares itself concurrency-safe so sibling calls may overlap,
|
|
198
|
+
* and exactly the three that ask the service to change something —
|
|
199
|
+
* `jules_create`, `jules_approve_plan`, `jules_send_message` — leave it unset so
|
|
200
|
+
* the pipeline serializes them against their siblings. Marking a mutating tool
|
|
201
|
+
* safe would let two approvals race, and marking a reader unsafe would make a
|
|
202
|
+
* batch of status checks run one at a time for no reason.
|
|
203
|
+
* @param ctx - plugin context supplying the tool registry.
|
|
204
|
+
* @param client - the configured Jules client.
|
|
205
|
+
* @param config - bounds applied to tool inputs.
|
|
206
|
+
*/
|
|
207
|
+
export function registerJulesTools(ctx, client, config) {
|
|
208
|
+
const seenByAgent = new WeakMap();
|
|
209
|
+
const seenWithoutAgent = new Map();
|
|
210
|
+
/**
|
|
211
|
+
* Record this look and report how long the same answer has been standing.
|
|
212
|
+
* @param agent - the calling agent, when there is one.
|
|
213
|
+
* @param session - bare Jules session id.
|
|
214
|
+
* @param signature - everything that would change if the session moved.
|
|
215
|
+
* @returns milliseconds since the identical observation, or 0 when it is new.
|
|
216
|
+
*/
|
|
217
|
+
const observe = (agent, session, signature) => {
|
|
218
|
+
let store;
|
|
219
|
+
if (agent === undefined) {
|
|
220
|
+
store = seenWithoutAgent;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const existing = seenByAgent.get(agent);
|
|
224
|
+
if (existing === undefined) {
|
|
225
|
+
store = new Map();
|
|
226
|
+
seenByAgent.set(agent, store);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
store = existing;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const previous = store.get(session);
|
|
233
|
+
const at = Date.now();
|
|
234
|
+
store.set(session, { signature, at });
|
|
235
|
+
return previous !== undefined && previous.signature === signature ? at - previous.at : 0;
|
|
236
|
+
};
|
|
237
|
+
ctx.tools.register(defineTool({
|
|
238
|
+
name: 'jules_sources',
|
|
239
|
+
description: 'List the repositories connected to Jules. A repository must appear here before jules_create can target it. '
|
|
240
|
+
+ 'Jules is a remote coding agent: it clones the repository, plans, edits files, and can open a pull request on its own.',
|
|
241
|
+
parameters: {
|
|
242
|
+
pageSize: { type: 'number', description: 'Repositories per page, 1-100.' },
|
|
243
|
+
pageToken: { type: 'string', description: 'Continuation token from a previous call.' },
|
|
244
|
+
},
|
|
245
|
+
output: {
|
|
246
|
+
schema: {
|
|
247
|
+
type: 'object', additionalProperties: false,
|
|
248
|
+
properties: {
|
|
249
|
+
sources: { type: 'array', required: true, items: SOURCE_SCHEMA },
|
|
250
|
+
nextPageToken: { type: 'string', required: true },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
render: (_args, value) => {
|
|
254
|
+
const body = renderSources(value.sources);
|
|
255
|
+
return [{
|
|
256
|
+
type: 'text',
|
|
257
|
+
text: value.nextPageToken.length === 0 ? body : `${body}\n(nextPageToken: ${value.nextPageToken})`,
|
|
258
|
+
}];
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
isConcurrencySafe: () => true,
|
|
262
|
+
async execute(args, exec) {
|
|
263
|
+
const response = await client.listSources({
|
|
264
|
+
...args.pageSize === undefined ? {} : { pageSize: args.pageSize },
|
|
265
|
+
...args.pageToken === undefined ? {} : { pageToken: args.pageToken },
|
|
266
|
+
signal: exec.signal,
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
sources: (response.sources ?? []).map(sourceRow),
|
|
270
|
+
nextPageToken: response.nextPageToken ?? '',
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
}));
|
|
274
|
+
ctx.tools.register(defineTool({
|
|
275
|
+
name: 'jules_create',
|
|
276
|
+
description: 'Start a Jules session: give the remote agent a task in a repository and return immediately with a session id. '
|
|
277
|
+
+ 'Jules works asynchronously in the cloud, so follow up with jules_wait or jules_status rather than expecting the work to be done. '
|
|
278
|
+
+ 'Set requirePlanApproval to review the plan before any file is edited, and autoCreatePr to have Jules open a pull request when it finishes. '
|
|
279
|
+
+ 'Omit source to run a repoless session that only needs reasoning or new files.',
|
|
280
|
+
parameters: {
|
|
281
|
+
prompt: { type: 'string', required: true, description: 'The task, as you would brief a capable engineer. Be specific about the desired outcome.' },
|
|
282
|
+
source: { type: 'string', description: 'Repository as "owner/repo" or "sources/github/owner/repo". Omit for a repoless session.' },
|
|
283
|
+
branch: { type: 'string', description: 'Branch Jules starts from. Defaults to the repository default branch.' },
|
|
284
|
+
title: { type: 'string', description: 'Short session title. Jules generates one when omitted.' },
|
|
285
|
+
requirePlanApproval: { type: 'boolean', description: 'Wait for an explicit jules_approve_plan before editing files. Recommended for changes to existing code.' },
|
|
286
|
+
autoCreatePr: { type: 'boolean', description: 'Open a pull request automatically when the session completes.' },
|
|
287
|
+
},
|
|
288
|
+
output: {
|
|
289
|
+
schema: SESSION_DETAIL_SCHEMA,
|
|
290
|
+
render: (_args, value) => [{
|
|
291
|
+
type: 'text',
|
|
292
|
+
text: `Started Jules session ${value.id} (${value.state}).\n${renderSessionDetail(value)}\n`
|
|
293
|
+
+ (value.requirePlanApproval
|
|
294
|
+
? `Next: jules_wait with session "${value.id}" until the plan is ready, then jules_approve_plan.`
|
|
295
|
+
: `Next: jules_wait with session "${value.id}" to follow it to completion.`),
|
|
296
|
+
}],
|
|
297
|
+
},
|
|
298
|
+
async execute(args, exec) {
|
|
299
|
+
if (args.prompt.trim().length === 0)
|
|
300
|
+
throw new Error('jules_create requires a non-empty prompt');
|
|
301
|
+
const source = args.source ?? (config.defaultSource.length === 0 ? undefined : config.defaultSource);
|
|
302
|
+
const session = await client.createSession({
|
|
303
|
+
prompt: args.prompt,
|
|
304
|
+
...args.title === undefined ? {} : { title: args.title },
|
|
305
|
+
...args.requirePlanApproval === undefined ? {} : { requirePlanApproval: args.requirePlanApproval },
|
|
306
|
+
...args.autoCreatePr === true ? { automationMode: 'AUTO_CREATE_PR' } : {},
|
|
307
|
+
...source === undefined || source.trim().length === 0 ? {} : {
|
|
308
|
+
sourceContext: {
|
|
309
|
+
source: sourceNameOf(source),
|
|
310
|
+
...args.branch === undefined ? {} : { githubRepoContext: { startingBranch: args.branch } },
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
}, exec.signal);
|
|
314
|
+
return sessionDetail(session);
|
|
315
|
+
},
|
|
316
|
+
}));
|
|
317
|
+
ctx.tools.register(defineTool({
|
|
318
|
+
name: 'jules_list',
|
|
319
|
+
description: 'List recent Jules sessions visible to this credential, newest first. A one-off survey — not a way to wait for progress.',
|
|
320
|
+
parameters: {
|
|
321
|
+
pageSize: { type: 'number', description: 'Sessions per page, 1-100.' },
|
|
322
|
+
pageToken: { type: 'string', description: 'Continuation token from a previous call.' },
|
|
323
|
+
},
|
|
324
|
+
output: {
|
|
325
|
+
schema: {
|
|
326
|
+
type: 'object', additionalProperties: false,
|
|
327
|
+
properties: {
|
|
328
|
+
sessions: { type: 'array', required: true, items: SESSION_ROW_SCHEMA },
|
|
329
|
+
nextPageToken: { type: 'string', required: true },
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
render: (_args, value) => [{ type: 'text', text: renderSessionRows(value.sessions, value.nextPageToken) }],
|
|
333
|
+
},
|
|
334
|
+
isConcurrencySafe: () => true,
|
|
335
|
+
async execute(args, exec) {
|
|
336
|
+
const response = await client.listSessions({
|
|
337
|
+
...args.pageSize === undefined ? {} : { pageSize: args.pageSize },
|
|
338
|
+
...args.pageToken === undefined ? {} : { pageToken: args.pageToken },
|
|
339
|
+
signal: exec.signal,
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
sessions: (response.sessions ?? []).map(sessionRow),
|
|
343
|
+
nextPageToken: response.nextPageToken ?? '',
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
}));
|
|
347
|
+
ctx.tools.register(defineTool({
|
|
348
|
+
name: 'jules_status',
|
|
349
|
+
description: 'Read one Jules session in full: state, generated plan, pull requests, produced files, and the latest command. '
|
|
350
|
+
+ 'Call it once to confirm an action you just took, to follow up a jules_watch notice, or when the user asks about a session — '
|
|
351
|
+
+ 'NOT in a loop to wait for progress. Waiting is jules_watch (background) or jules_wait (foreground); both report this same '
|
|
352
|
+
+ 'detail when they settle, so repeating this call only burns turns.',
|
|
353
|
+
parameters: {
|
|
354
|
+
session: { type: 'string', required: true, description: 'Session id, "sessions/<id>", or the session URL.' },
|
|
355
|
+
},
|
|
356
|
+
output: {
|
|
357
|
+
schema: SESSION_DETAIL_SCHEMA,
|
|
358
|
+
render: (_args, value) => [{ type: 'text', text: renderSessionDetail(value) }],
|
|
359
|
+
},
|
|
360
|
+
isConcurrencySafe: () => true,
|
|
361
|
+
async execute(args, exec) {
|
|
362
|
+
const id = sessionIdOf(args.session);
|
|
363
|
+
const { session, activities, logRead } = await readSessionDetail(client, id, exec.signal, config.maxActivityPages);
|
|
364
|
+
// Everything that would differ if the session had moved. A repeat of the
|
|
365
|
+
// same signature is the signal that this call was not worth making.
|
|
366
|
+
const signature = [
|
|
367
|
+
session.state ?? '',
|
|
368
|
+
session.updateTime ?? '',
|
|
369
|
+
String(activities.length),
|
|
370
|
+
activities[activities.length - 1]?.id ?? '',
|
|
371
|
+
].join('|');
|
|
372
|
+
return sessionDetail(session, activities, { unchangedForMs: observe(exec.agent, id, signature), logRead });
|
|
373
|
+
},
|
|
374
|
+
}));
|
|
375
|
+
ctx.tools.register(defineTool({
|
|
376
|
+
name: 'jules_activities',
|
|
377
|
+
description: 'Read a Jules session event log, oldest first: plan generation and approval, agent and user messages, progress updates, '
|
|
378
|
+
+ 'completion or failure, and a summary of every artifact. Use it to see what the agent actually did, to diagnose a stall, or to '
|
|
379
|
+
+ 'follow up a jules_watch notice — not to poll for progress.',
|
|
380
|
+
parameters: {
|
|
381
|
+
session: { type: 'string', required: true, description: 'Session id, "sessions/<id>", or the session URL.' },
|
|
382
|
+
pageSize: { type: 'number', description: 'Activities per page, 1-100.' },
|
|
383
|
+
pageToken: { type: 'string', description: 'Continuation token from a previous call.' },
|
|
384
|
+
since: { type: 'string', description: 'RFC 3339 timestamp; return only activities created after it, for polling a long session.' },
|
|
385
|
+
},
|
|
386
|
+
output: {
|
|
387
|
+
schema: {
|
|
388
|
+
type: 'object', additionalProperties: false,
|
|
389
|
+
properties: {
|
|
390
|
+
activities: { type: 'array', required: true, items: ACTIVITY_SCHEMA },
|
|
391
|
+
nextPageToken: { type: 'string', required: true },
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
render: (_args, value) => [{ type: 'text', text: renderActivities(value.activities, value.nextPageToken) }],
|
|
395
|
+
},
|
|
396
|
+
isConcurrencySafe: () => true,
|
|
397
|
+
async execute(args, exec) {
|
|
398
|
+
const id = sessionIdOf(args.session);
|
|
399
|
+
const response = await client.listActivities(id, {
|
|
400
|
+
...args.pageSize === undefined ? {} : { pageSize: args.pageSize },
|
|
401
|
+
...args.pageToken === undefined ? {} : { pageToken: args.pageToken },
|
|
402
|
+
...args.since === undefined ? {} : { since: args.since },
|
|
403
|
+
signal: exec.signal,
|
|
404
|
+
});
|
|
405
|
+
return {
|
|
406
|
+
activities: (response.activities ?? []).map(activityRow),
|
|
407
|
+
nextPageToken: response.nextPageToken ?? '',
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
}));
|
|
411
|
+
ctx.tools.register(defineTool({
|
|
412
|
+
name: 'jules_approve_plan',
|
|
413
|
+
description: 'Approve the plan a Jules session is waiting on, which lets the agent start editing files. '
|
|
414
|
+
+ 'Only call this after jules_status or jules_activities showed the plan and you consider it correct. '
|
|
415
|
+
+ 'To change it instead, use jules_send_message with the corrections.',
|
|
416
|
+
parameters: {
|
|
417
|
+
session: { type: 'string', required: true, description: 'Session id, "sessions/<id>", or the session URL.' },
|
|
418
|
+
},
|
|
419
|
+
output: {
|
|
420
|
+
schema: SESSION_DETAIL_SCHEMA,
|
|
421
|
+
render: (_args, value) => [{
|
|
422
|
+
type: 'text',
|
|
423
|
+
text: approvalLead(value.id, value.approvalConfirmed, value.logRead) + '\n' + renderSessionDetail(value),
|
|
424
|
+
}],
|
|
425
|
+
},
|
|
426
|
+
async execute(args, exec) {
|
|
427
|
+
const id = sessionIdOf(args.session);
|
|
428
|
+
await client.approvePlan(id, exec.signal);
|
|
429
|
+
// :approvePlan returning is not evidence that the approval is readable.
|
|
430
|
+
// The service is eventually consistent about its own actions, so its
|
|
431
|
+
// planApproved event lags — and a snapshot taken immediately reported
|
|
432
|
+
// planApproved false, or a phase that had already moved on, for approvals
|
|
433
|
+
// that had in fact landed. Poll for the event and report whether it was
|
|
434
|
+
// seen, rather than handing back the race.
|
|
435
|
+
const read = () => client.listAllActivities(id, {
|
|
436
|
+
signal: exec.signal,
|
|
437
|
+
maxPages: config.maxActivityPages,
|
|
438
|
+
pageSize: 100,
|
|
439
|
+
});
|
|
440
|
+
// A read failure here must not be reported as an unconfirmed approval, nor
|
|
441
|
+
// thrown away: the approval may well have landed, and the caller needs to
|
|
442
|
+
// know that we could not look rather than that we looked and saw nothing.
|
|
443
|
+
let activities = [];
|
|
444
|
+
let logRead = true;
|
|
445
|
+
let confirmed = false;
|
|
446
|
+
try {
|
|
447
|
+
activities = await read();
|
|
448
|
+
confirmed = activities.some(isPlanApproved);
|
|
449
|
+
for (let attempt = 1; !confirmed && attempt <= APPROVAL_CONFIRM_ATTEMPTS; attempt += 1) {
|
|
450
|
+
await sleep(APPROVAL_CONFIRM_DELAY_MS * attempt, exec.signal);
|
|
451
|
+
activities = await read();
|
|
452
|
+
confirmed = activities.some(isPlanApproved);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (exec.signal.aborted)
|
|
457
|
+
throw error;
|
|
458
|
+
logRead = false;
|
|
459
|
+
}
|
|
460
|
+
const session = await client.getSession(id, exec.signal);
|
|
461
|
+
return sessionDetail(session, activities, { approvalConfirmed: confirmed, logRead });
|
|
462
|
+
},
|
|
463
|
+
}));
|
|
464
|
+
ctx.tools.register(defineTool({
|
|
465
|
+
name: 'jules_send_message',
|
|
466
|
+
description: 'Send a message to a Jules session: answer a question it asked, correct its plan, or give it follow-up work. '
|
|
467
|
+
+ 'The agent replies asynchronously, so read jules_activities afterwards.',
|
|
468
|
+
parameters: {
|
|
469
|
+
session: { type: 'string', required: true, description: 'Session id, "sessions/<id>", or the session URL.' },
|
|
470
|
+
prompt: { type: 'string', required: true, description: 'What to tell the agent.' },
|
|
471
|
+
},
|
|
472
|
+
output: {
|
|
473
|
+
schema: SESSION_DETAIL_SCHEMA,
|
|
474
|
+
render: (_args, value) => [{
|
|
475
|
+
type: 'text',
|
|
476
|
+
text: `Sent the message to session ${value.id} (now ${value.state}).\n${renderSessionDetail(value)}`,
|
|
477
|
+
}],
|
|
478
|
+
},
|
|
479
|
+
async execute(args, exec) {
|
|
480
|
+
if (args.prompt.trim().length === 0)
|
|
481
|
+
throw new Error('jules_send_message requires a non-empty prompt');
|
|
482
|
+
const id = sessionIdOf(args.session);
|
|
483
|
+
await client.sendMessage(id, args.prompt, exec.signal);
|
|
484
|
+
const { session, activities, logRead } = await readSessionDetail(client, id, exec.signal, config.maxActivityPages);
|
|
485
|
+
return sessionDetail(session, activities, { logRead });
|
|
486
|
+
},
|
|
487
|
+
}));
|
|
488
|
+
ctx.tools.register(defineTool({
|
|
489
|
+
name: 'jules_wait',
|
|
490
|
+
description: 'Hold the turn open until a Jules session finishes, fails, needs a plan decision, or posts a message, then report what '
|
|
491
|
+
+ 'happened. Use it only when you genuinely have nothing else to do until the answer arrives — prefer jules_watch, which waits in '
|
|
492
|
+
+ 'the background and lets your turn end. Neither is a licence to poll jules_status.',
|
|
493
|
+
parameters: {
|
|
494
|
+
session: { type: 'string', required: true, description: 'Session id, "sessions/<id>", or the session URL.' },
|
|
495
|
+
timeoutMs: { type: 'number', description: `How long to wait before giving up, up to ${Math.trunc(config.waitMaxMs / 1000)} seconds.` },
|
|
496
|
+
until: {
|
|
497
|
+
type: 'array',
|
|
498
|
+
description: 'Session states that end the wait. Defaults to completion, failure, a pending plan, and a question.',
|
|
499
|
+
items: { type: 'string' },
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
output: {
|
|
503
|
+
schema: WAIT_SCHEMA,
|
|
504
|
+
render: (_args, value) => [{ type: 'text', text: renderWait(value) }],
|
|
505
|
+
},
|
|
506
|
+
// The tool's own deadline must outlast the wait budget it is given, or the
|
|
507
|
+
// pipeline would cut the call off before the wait could report. The margin
|
|
508
|
+
// covers the reads that follow settlement: an activity walk and a status
|
|
509
|
+
// fetch, each of which can take seconds against this service.
|
|
510
|
+
timeoutMs: config.waitMaxMs + 15_000,
|
|
511
|
+
isConcurrencySafe: () => true,
|
|
512
|
+
// The foreground wait shares runWatch with jules_watch. Duplicating the loop
|
|
513
|
+
// here is what let the two paths disagree: this one used to poll the session
|
|
514
|
+
// state alone, so a session that posted its answer while state stayed
|
|
515
|
+
// IN_PROGRESS would wait out the whole budget and report nothing.
|
|
516
|
+
async execute(args, exec) {
|
|
517
|
+
const id = sessionIdOf(args.session);
|
|
518
|
+
const requested = args.timeoutMs ?? config.waitDefaultMs;
|
|
519
|
+
const budget = Math.max(1_000, Math.min(Math.trunc(requested), config.waitMaxMs));
|
|
520
|
+
const targets = new Set(args.until === undefined || args.until.length === 0 ? DEFAULT_WAIT_STATES : args.until);
|
|
521
|
+
const outcome = await runWatch(client, id, targets, budget, {
|
|
522
|
+
pollIntervalMs: config.pollIntervalMs,
|
|
523
|
+
maxActivityPages: config.maxActivityPages,
|
|
524
|
+
settleOnMessage: config.settleOnMessage,
|
|
525
|
+
}, exec.signal);
|
|
526
|
+
return outcome.wait;
|
|
527
|
+
},
|
|
528
|
+
}));
|
|
529
|
+
ctx.tools.register(defineTool({
|
|
530
|
+
name: 'jules_patch',
|
|
531
|
+
description: 'Return the unified diff a Jules session produced, newest first, ready to apply with git apply. '
|
|
532
|
+
+ 'Use it to review or land the agent work in this workspace instead of opening the pull request. Large diffs are '
|
|
533
|
+
+ 'returned in slices: the reply lists every touched file and, when it truncates, the offset to continue from.',
|
|
534
|
+
parameters: {
|
|
535
|
+
session: { type: 'string', required: true, description: 'Session id, "sessions/<id>", or the session URL.' },
|
|
536
|
+
maxBytes: { type: 'number', description: `Cap on this slice, up to ${config.maxPatchBytes} bytes.` },
|
|
537
|
+
offset: { type: 'number', description: 'Byte offset to start this slice at. Use the nextOffset from a truncated reply to continue.' },
|
|
538
|
+
},
|
|
539
|
+
output: {
|
|
540
|
+
schema: PATCH_SCHEMA,
|
|
541
|
+
render: (_args, value) => [{ type: 'text', text: renderPatch(value) }],
|
|
542
|
+
},
|
|
543
|
+
isConcurrencySafe: () => true,
|
|
544
|
+
async execute(args, exec) {
|
|
545
|
+
const id = sessionIdOf(args.session);
|
|
546
|
+
const activities = await client.listAllActivities(id, { signal: exec.signal, maxPages: config.maxActivityPages, pageSize: 100 });
|
|
547
|
+
const gitPatch = latestPatch(activities);
|
|
548
|
+
const full = gitPatch?.unidiffPatch ?? '';
|
|
549
|
+
const limit = Math.max(1_000, Math.min(Math.trunc(args.maxBytes ?? config.maxPatchBytes), config.maxPatchBytes));
|
|
550
|
+
if (full.length === 0) {
|
|
551
|
+
return {
|
|
552
|
+
id, found: false, patch: '', bytes: 0, truncated: false,
|
|
553
|
+
offset: 0, nextOffset: 0, files: [],
|
|
554
|
+
baseCommitId: '', suggestedCommitMessage: '', source: '',
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
const offset = Math.max(0, Math.min(Math.trunc(args.offset ?? 0), Math.max(0, full.length - 1)));
|
|
558
|
+
const slice = full.slice(offset, offset + limit);
|
|
559
|
+
const truncated = offset + slice.length < full.length;
|
|
560
|
+
return {
|
|
561
|
+
id,
|
|
562
|
+
found: true,
|
|
563
|
+
patch: slice,
|
|
564
|
+
// The size of the whole diff, so one slice still says how much there is.
|
|
565
|
+
bytes: full.length,
|
|
566
|
+
truncated,
|
|
567
|
+
offset,
|
|
568
|
+
nextOffset: truncated ? offset + slice.length : 0,
|
|
569
|
+
files: patchFiles(full),
|
|
570
|
+
baseCommitId: gitPatch?.baseCommitId ?? '',
|
|
571
|
+
suggestedCommitMessage: gitPatch?.suggestedCommitMessage ?? '',
|
|
572
|
+
source: '',
|
|
573
|
+
};
|
|
574
|
+
},
|
|
575
|
+
}));
|
|
576
|
+
}
|