@adibacsi/pi-jack 1.0.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/agents/tester.md +9 -0
- package/agents/worker.md +11 -0
- package/agents.ts +178 -0
- package/contract.ts +278 -0
- package/index.ts +930 -0
- package/json-schema.ts +165 -0
- package/package.json +76 -0
- package/prompts/jack-demo.md +54 -0
package/json-schema.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--json-schema <schema>`: makes a pi run finish with an answer that conforms to a JSON Schema. The value is an
|
|
3
|
+
* inline JSON Schema or a path to a `.json` file (relative to the working directory). Part of JACK: the parent starts
|
|
4
|
+
* every subagent with it, and it works the same in any pi run, e.g.
|
|
5
|
+
*
|
|
6
|
+
* pi --mode json -p --json-schema ./schema.json "Summarize this repository"
|
|
7
|
+
*
|
|
8
|
+
* Registers the two tools such a run finishes with (see contract.ts):
|
|
9
|
+
* - `jack_subagent_result`: parameters are the run's JSON Schema, with provider-side constrained sampling requested.
|
|
10
|
+
* Pi validates the arguments against it before the tool runs, and the tool checks them again; either way an
|
|
11
|
+
* invalid call is thrown back with the errors so the model can fix it. For a subagent, JACK's parent stops the
|
|
12
|
+
* child once it has rejected more than MAX_FORMAT_RETRIES answers, since only it sees both kinds of rejection.
|
|
13
|
+
* - `jack_subagent_fail({ reason })`: ends the run when the task cannot be completed.
|
|
14
|
+
* If the agent is about to finish without calling either, a hidden message nudges it, up to MAX_FORMAT_RETRIES times.
|
|
15
|
+
*
|
|
16
|
+
* With `--mode json`, the answer is `result.details` of the last successful `tool_execution_end` event of
|
|
17
|
+
* `jack_subagent_result` (or the reason, for `jack_subagent_fail`); that is where JACK's parent reads it.
|
|
18
|
+
* Does nothing unless `--json-schema` is set.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
defineTool,
|
|
23
|
+
type ExtensionAPI,
|
|
24
|
+
type ExtensionContext,
|
|
25
|
+
} from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { Type } from "typebox";
|
|
27
|
+
import {
|
|
28
|
+
FAIL_TOOL,
|
|
29
|
+
MAX_FORMAT_RETRIES,
|
|
30
|
+
RESULT_TOOL,
|
|
31
|
+
resolveSchema,
|
|
32
|
+
SCHEMA_FLAG,
|
|
33
|
+
schemaErrors,
|
|
34
|
+
} from "./contract.ts";
|
|
35
|
+
|
|
36
|
+
export function registerJsonSchema(pi: ExtensionAPI): void {
|
|
37
|
+
pi.registerFlag(SCHEMA_FLAG, {
|
|
38
|
+
description:
|
|
39
|
+
"JSON Schema (inline JSON or path to a .json file) that the final answer must conform to",
|
|
40
|
+
type: "string",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Per-run state.
|
|
44
|
+
let finished = false;
|
|
45
|
+
let nudges = 0;
|
|
46
|
+
|
|
47
|
+
const report = (ctx: ExtensionContext, message: string) => {
|
|
48
|
+
if (ctx.hasUI) ctx.ui.notify(message, "error");
|
|
49
|
+
console.error(message);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
pi.on("session_start", (_event, ctx) => {
|
|
53
|
+
const schemaPath = pi.getFlag(SCHEMA_FLAG);
|
|
54
|
+
if (typeof schemaPath !== "string" || !schemaPath) return;
|
|
55
|
+
|
|
56
|
+
let schema;
|
|
57
|
+
try {
|
|
58
|
+
schema = resolveSchema(schemaPath, ctx.cwd);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
report(
|
|
61
|
+
ctx,
|
|
62
|
+
`[jack] Could not load the schema: ${e instanceof Error ? e.message : e}`,
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pi.registerTool(
|
|
68
|
+
defineTool({
|
|
69
|
+
name: RESULT_TOOL,
|
|
70
|
+
label: "Subagent Result",
|
|
71
|
+
description:
|
|
72
|
+
"Submit your final answer to the agent that delegated this task. Call it exactly once, as your last " +
|
|
73
|
+
"action, when the task is done. The parameters are the answer: fill each one as its description says.",
|
|
74
|
+
promptSnippet: "Submit your final answer as structured data",
|
|
75
|
+
promptGuidelines: [
|
|
76
|
+
`Finish by calling ${RESULT_TOOL} exactly once with your answer; its parameters define what to return.`,
|
|
77
|
+
`Your text replies are not returned to anyone; only the arguments of ${RESULT_TOOL} are.`,
|
|
78
|
+
`If you cannot complete the task, call ${FAIL_TOOL} with the reason instead of ${RESULT_TOOL}.`,
|
|
79
|
+
],
|
|
80
|
+
parameters: schema,
|
|
81
|
+
// "prefer" falls back to plain tool calling on providers without strict JSON-schema sampling;
|
|
82
|
+
// the validation below applies either way.
|
|
83
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
84
|
+
async execute(_toolCallId, params) {
|
|
85
|
+
const errors = schemaErrors(schema, params);
|
|
86
|
+
if (errors.length === 0) {
|
|
87
|
+
finished = true;
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: JSON.stringify(params) }],
|
|
90
|
+
details: params,
|
|
91
|
+
terminate: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const list = errors.map((e) => `- ${e}`).join("\n");
|
|
95
|
+
throw new Error(
|
|
96
|
+
`The answer does not match the schema. Fix these and call ${RESULT_TOOL} again:\n${list}`,
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
pi.registerTool(
|
|
103
|
+
defineTool({
|
|
104
|
+
name: FAIL_TOOL,
|
|
105
|
+
label: "Subagent Fail",
|
|
106
|
+
description:
|
|
107
|
+
`Give up on the task. Call it instead of ${RESULT_TOOL}, as your last action, only when the task ` +
|
|
108
|
+
"cannot be completed; the delegating agent receives the reason as the error.",
|
|
109
|
+
parameters: Type.Object({
|
|
110
|
+
reason: Type.String({
|
|
111
|
+
description:
|
|
112
|
+
"One or two sentences: what stopped you, and what would unblock it.",
|
|
113
|
+
}),
|
|
114
|
+
}),
|
|
115
|
+
async execute(_toolCallId, params) {
|
|
116
|
+
finished = true;
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: params.reason }],
|
|
119
|
+
details: params,
|
|
120
|
+
terminate: true,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// The parent adds both tools to any `--tools` list; this keeps them active under the default tool set too.
|
|
127
|
+
const active = pi.getActiveTools();
|
|
128
|
+
const missing = [RESULT_TOOL, FAIL_TOOL].filter((t) => !active.includes(t));
|
|
129
|
+
if (missing.length > 0) pi.setActiveTools([...active, ...missing]);
|
|
130
|
+
if (!pi.getActiveTools().includes(RESULT_TOOL)) {
|
|
131
|
+
report(
|
|
132
|
+
ctx,
|
|
133
|
+
`[jack] The ${RESULT_TOOL} tool is not available; it must not be excluded from the tool list.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
pi.on("before_agent_start", () => {
|
|
139
|
+
finished = false;
|
|
140
|
+
nudges = 0;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.on("agent_before_settle", () => {
|
|
144
|
+
if (
|
|
145
|
+
finished ||
|
|
146
|
+
!pi.getActiveTools().includes(RESULT_TOOL) ||
|
|
147
|
+
nudges >= MAX_FORMAT_RETRIES
|
|
148
|
+
)
|
|
149
|
+
return;
|
|
150
|
+
nudges++;
|
|
151
|
+
return {
|
|
152
|
+
continue: true,
|
|
153
|
+
entries: [
|
|
154
|
+
{
|
|
155
|
+
type: "custom_message",
|
|
156
|
+
customType: "jack-result-nudge",
|
|
157
|
+
display: false,
|
|
158
|
+
content:
|
|
159
|
+
`You have not submitted an answer. Call ${RESULT_TOOL} now with your answer as its arguments ` +
|
|
160
|
+
`(or ${FAIL_TOOL} with the reason if you cannot complete the task).`,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adibacsi/pi-jack",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "JACK (JSON Agent Contractor Kit): Pi extension for delegating to subagents under a JSON Schema contract, for structured, reliable, and type-safe agent responses.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi",
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"subagents",
|
|
10
|
+
"json-schema"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/adamjakab/pi-jack#readme",
|
|
13
|
+
"bugs": "https://github.com/adamjakab/pi-jack/issues",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/adamjakab/pi-jack.git"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Adam Jakab",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "index.ts",
|
|
22
|
+
"exports": "./index.ts",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.19.0"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"index.ts",
|
|
28
|
+
"agents.ts",
|
|
29
|
+
"contract.ts",
|
|
30
|
+
"json-schema.ts",
|
|
31
|
+
"agents/",
|
|
32
|
+
"prompts/",
|
|
33
|
+
"CHANGELOG.md"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./index.ts"
|
|
41
|
+
],
|
|
42
|
+
"prompts": [
|
|
43
|
+
"./prompts/*.md"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"test:e2e": "node tests/e2e/run.ts",
|
|
50
|
+
"typecheck": "node scripts/pi-modules.mjs && tsc -p tsconfig.json",
|
|
51
|
+
"format": "prettier --write .",
|
|
52
|
+
"format:check": "prettier --check ."
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@earendil-works/pi-ai": "*",
|
|
56
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
57
|
+
"typebox": "*"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@earendil-works/pi-ai": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@earendil-works/pi-coding-agent": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"typebox": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@types/node": "^26.6.4",
|
|
72
|
+
"prettier": "^3.9.9",
|
|
73
|
+
"typescript": "^7.0.2",
|
|
74
|
+
"vitest": "^5.0.3"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Demo of jack, four read-only subagents surveying a folder in parallel
|
|
3
|
+
argument-hint: "[path]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Subagents Demo
|
|
7
|
+
|
|
8
|
+
Demonstrate the `jack` tool by surveying ${1:-the current folder} with multiple tasks carried out by independent agents.
|
|
9
|
+
|
|
10
|
+
Call `jack` exactly **once**, with these parameters and nothing else:
|
|
11
|
+
|
|
12
|
+
- debug_mode: false
|
|
13
|
+
- run_mode: `parallel`
|
|
14
|
+
- agent: `tester`
|
|
15
|
+
- model: `stealth/space-bunny-alpha`
|
|
16
|
+
- thinking: off
|
|
17
|
+
- schema:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"topic": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "The topic name from the square brackets in your task, e.g. LAYOUT."
|
|
26
|
+
},
|
|
27
|
+
"findings": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": { "type": "string" },
|
|
30
|
+
"description": "What you found, one self-contained item per entry."
|
|
31
|
+
},
|
|
32
|
+
"summary": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "One or two sentences summing up the findings."
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"required": ["topic", "findings", "summary"],
|
|
38
|
+
"additionalProperties": false
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `tasks` (topic names are in the square brackets):
|
|
43
|
+
- [LAYOUT] Layout of ${1:-the current folder}: list its top-level entries and say in a few words what each one is for.
|
|
44
|
+
- [FTYPE] File types in ${1:-the current folder}: count files per extension, ignoring .git and node_modules, and report the five most common.
|
|
45
|
+
- [NOTES] Open notes in ${1:-the current folder}: find TODO, FIXME and HACK comments, ignoring .git and node_modules, and report up to ten as "file:line: text".
|
|
46
|
+
- [DOCS] Docs in ${1:-the current folder}: read its README.md or AGENTS.md (whichever exists, top level only) and summarize what the project is in three findings. Use `medium` thinking level for this.
|
|
47
|
+
|
|
48
|
+
Do not do any of this work yourself, before or after the call: the point is to watch the subagents do it.
|
|
49
|
+
|
|
50
|
+
When the call returns, report:
|
|
51
|
+
|
|
52
|
+
1. A table with one row per task: task number, topic, and ✓ or ✗.
|
|
53
|
+
2. Each subagent's `summary` and `findings`, under its topic as a heading.
|
|
54
|
+
3. For any failed task, its `error`.
|