@aion0/forge 0.10.29 → 0.10.31
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/RELEASE_NOTES.md +4 -13
- package/lib/chat/agent-loop.ts +22 -1
- package/lib/watch/start-watch-tool.ts +15 -8
- package/lib/watch/watch-runner.ts +4 -1
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
# Forge v0.10.
|
|
1
|
+
# Forge v0.10.31
|
|
2
2
|
|
|
3
3
|
Released: 2026-06-02
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.30
|
|
6
6
|
|
|
7
7
|
### Other
|
|
8
|
-
-
|
|
9
|
-
- fix(help-content): drop import.meta.url branch — Turbopack rejects webpackIgnore
|
|
10
|
-
- fix(help-content): suppress Turbopack 'cannot resolve ./help-docs' warning
|
|
11
|
-
- fix(http): empty-string body resolves to no-body
|
|
12
|
-
- fix(http-auth): bearer-token-exchange honours connector http.verify_tls
|
|
13
|
-
- feat(connector-test): add body_match regex for response-body validation
|
|
14
|
-
- fix(http): reorder method-template expand to AFTER argsWithDefaults
|
|
15
|
-
- fix(http): expand templates in spec.method before uppercase
|
|
16
|
-
- docs(connector-authoring): add bearer-token-exchange section to 21-build-connector
|
|
17
|
-
- feat(http-auth): bearer-token-exchange supports body-mode + bare format
|
|
8
|
+
- fix(watch): allow builtin tool names (no connector prefix) in start_watch
|
|
18
9
|
|
|
19
10
|
|
|
20
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
11
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.30...v0.10.31
|
package/lib/chat/agent-loop.ts
CHANGED
|
@@ -41,7 +41,11 @@ import type {
|
|
|
41
41
|
ToolUseBlock,
|
|
42
42
|
} from './types';
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// Set to 24: enough headroom for typical multi-step tool chains
|
|
45
|
+
// (search → paginate → drill → summarise can be 8-15; complex audits
|
|
46
|
+
// 15-20) without inviting runaway loops. Hitting it surfaces the
|
|
47
|
+
// sentinel message below so the user knows why the turn stopped.
|
|
48
|
+
const MAX_ITERATIONS = 24;
|
|
45
49
|
const MAX_TOKENS = 16000;
|
|
46
50
|
// Working-window budgets for the LLM history. Capped by message count
|
|
47
51
|
// AND by token estimate (whichever hits first), see design §8. Older
|
|
@@ -596,6 +600,23 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
|
|
|
596
600
|
cb({ type: 'message_saved', message_id: toolResultMsg.id, data: toolResultMsg });
|
|
597
601
|
}
|
|
598
602
|
|
|
603
|
+
// If we fell out of the while because we hit MAX_ITERATIONS (vs the
|
|
604
|
+
// model emitting a non-tool_use stopReason), the turn ends with no
|
|
605
|
+
// closing assistant text and the user sees a "no response" hang.
|
|
606
|
+
// Surface a sentinel assistant message so the cause is visible.
|
|
607
|
+
if (iter >= MAX_ITERATIONS && lastStop === 'tool_use') {
|
|
608
|
+
const sentinel = appendMessage({
|
|
609
|
+
session_id: args.sessionId,
|
|
610
|
+
role: 'assistant',
|
|
611
|
+
blocks: [{
|
|
612
|
+
type: 'text',
|
|
613
|
+
text: `⚠️ Hit the tool-call iteration limit (${MAX_ITERATIONS}) without finishing. Likely causes: a tool kept returning truncated/incomplete results so I kept retrying, or the task was too broad. Try narrowing the query or splitting it into smaller asks.`,
|
|
614
|
+
}],
|
|
615
|
+
});
|
|
616
|
+
cb({ type: 'message_saved', message_id: sentinel.id, data: sentinel });
|
|
617
|
+
cb({ type: 'error', data: { error: `iteration limit (${MAX_ITERATIONS}) exceeded` } });
|
|
618
|
+
}
|
|
619
|
+
|
|
599
620
|
cb({ type: 'turn_done', data: { iterations: iter, stop_reason: lastStop } });
|
|
600
621
|
return { ok: true };
|
|
601
622
|
} catch (err) {
|
|
@@ -35,12 +35,12 @@ export function buildStartWatchTool(sessionId: string | null): StartWatchTool {
|
|
|
35
35
|
const def: BuiltinToolDef = {
|
|
36
36
|
name: 'start_watch',
|
|
37
37
|
description:
|
|
38
|
-
'Register a BACKGROUND WATCH that polls a tool until done, then posts the result back here — for long-running jobs you just kicked off (a Jenkins build, a test run, a device upgrade). Use this INSTEAD of polling in conversation: call the trigger tool, then call start_watch and STOP — Forge polls in the background and a completion message arrives in this chat. ' +
|
|
39
|
-
'Pick `poll` = the read tool that reports status (e.g. "jenkins.get_build") and `poll_args` to call it with (e.g. the build number you predicted via get_next_build_number). Give a done condition: `done_match` {path, equals} on the poll result (e.g. path "result" equals "SUCCESS"), or `done_path` (a result path that becomes truthy). You usually already saw the poll tool\'s output once, so you know the right field. Optional `fail_path` (truthy = failed). Tune `interval_sec`/`timeout_sec` to the job (build ≈ 60s / 1800s).',
|
|
38
|
+
'Register a BACKGROUND WATCH that polls a tool until done, then posts the result back here — for long-running jobs you just kicked off (a Forge pipeline, a Jenkins build, a test run, a device upgrade). Use this INSTEAD of polling in conversation: call the trigger tool, then call start_watch and STOP — Forge polls in the background and a completion message arrives in this chat. ' +
|
|
39
|
+
'Pick `poll` = the read tool that reports status. Two forms accepted: (a) connector tool "<connector>.<tool>" e.g. "jenkins.get_build", "gitlab.get_pipeline"; (b) BARE builtin name e.g. "get_pipeline_status" (Forge pipelines — pair with poll_args={pipeline_id} and done_match={path:"status",equals:"done"}). Set `poll_args` to call it with (e.g. the build number you predicted via get_next_build_number). Give a done condition: `done_match` {path, equals} on the poll result (e.g. path "result" equals "SUCCESS"), or `done_path` (a result path that becomes truthy). You usually already saw the poll tool\'s output once, so you know the right field. Optional `fail_path` (truthy = failed). Tune `interval_sec`/`timeout_sec` to the job (pipeline ≈ 30s / 1800s, build ≈ 60s / 1800s).',
|
|
40
40
|
input_schema: {
|
|
41
41
|
type: 'object',
|
|
42
42
|
properties: {
|
|
43
|
-
poll: { type: 'string', description: 'Tool to poll
|
|
43
|
+
poll: { type: 'string', description: 'Tool to poll. Either "<connector>.<tool>" e.g. "jenkins.get_build", OR a bare builtin name e.g. "get_pipeline_status" (for Forge pipelines). Must be a read/status tool.' },
|
|
44
44
|
poll_args: { type: 'object', description: 'Args to call the poll tool with each tick, e.g. {"job_path":"job/foo","build_number":18}. Concrete values, not templates.' },
|
|
45
45
|
done_match: {
|
|
46
46
|
type: 'object',
|
|
@@ -68,9 +68,16 @@ export function buildStartWatchTool(sessionId: string | null): StartWatchTool {
|
|
|
68
68
|
const a = (input ?? {}) as Record<string, any>;
|
|
69
69
|
const poll = String(a.poll || '').trim();
|
|
70
70
|
const dot = poll.indexOf('.');
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
// Two forms accepted:
|
|
72
|
+
// "connector.tool" → connector_id="connector", poll_tool="tool"
|
|
73
|
+
// "tool" → builtin (e.g. get_pipeline_status), connector_id=""
|
|
74
|
+
// The watch-runner detects empty connector_id and dispatches as a
|
|
75
|
+
// bare builtin name instead of "connector.tool".
|
|
76
|
+
const connectorId = dot >= 1 ? poll.slice(0, dot) : '';
|
|
77
|
+
const pollTool = dot >= 1 ? poll.slice(dot + 1) : poll;
|
|
78
|
+
if (!pollTool) {
|
|
79
|
+
return JSON.stringify({ ok: false, error: 'poll is required, e.g. "jenkins.get_build" or builtin "get_pipeline_status"' });
|
|
80
|
+
}
|
|
74
81
|
|
|
75
82
|
const doneMatch = a.done_match && typeof a.done_match === 'object' && a.done_match.path
|
|
76
83
|
? { path: String(a.done_match.path), ...(a.done_match.equals != null ? { equals: String(a.done_match.equals) } : {}), ...(a.done_match.contains != null ? { contains: String(a.done_match.contains) } : {}) }
|
|
@@ -83,8 +90,8 @@ export function buildStartWatchTool(sessionId: string | null): StartWatchTool {
|
|
|
83
90
|
return JSON.stringify({ ok: false, error: `active watch limit reached (${MAX_ACTIVE_WATCHES})` });
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
const hint = ['build_number', 'host', 'ip', 'lab', 'id', 'name'].map((k) => a.poll_args?.[k]).find((v) => v != null && v !== '');
|
|
87
|
-
const label = `${connectorId}.${pollTool}${hint != null ? ` ${hint}` : ''}`;
|
|
93
|
+
const hint = ['build_number', 'host', 'ip', 'lab', 'id', 'name', 'pipeline_id'].map((k) => a.poll_args?.[k]).find((v) => v != null && v !== '');
|
|
94
|
+
const label = `${connectorId ? `${connectorId}.${pollTool}` : pollTool}${hint != null ? ` ${hint}` : ''}`;
|
|
88
95
|
|
|
89
96
|
const w = createWatch({
|
|
90
97
|
session_id: sessionId,
|
|
@@ -93,7 +93,10 @@ export function startWatchRunner(hooks: WatchRunnerHooks = {}): void {
|
|
|
93
93
|
// preamble — the body comes back as raw JSON so done_path/done_match can
|
|
94
94
|
// see it. (Without this, every http-protocol watch would silently never
|
|
95
95
|
// hit its done condition, e.g. jenkins.get_build never resolving.)
|
|
96
|
-
|
|
96
|
+
// Empty connector_id → builtin (e.g. get_pipeline_status); dispatch
|
|
97
|
+
// bare name so dispatchTool routes to the global BUILTINS table.
|
|
98
|
+
const dispatchName = w.connector_id ? `${w.connector_id}.${w.poll_tool}` : w.poll_tool;
|
|
99
|
+
res = await dispatchTool({ id: `watch-${w.id}-${w.polls}`, name: dispatchName, input: w.poll_args }, { noTruncation: true } as any);
|
|
97
100
|
} catch (e) {
|
|
98
101
|
res = { content: String(e), is_error: true };
|
|
99
102
|
}
|
package/package.json
CHANGED