@tangle-network/chatgpt-agents-kit 0.1.0 → 0.1.2
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/CARDS.md +135 -0
- package/README.md +45 -59
- package/SETUP.md +43 -7
- package/dist/cards/server.d.ts +6 -0
- package/dist/cards/server.js +11 -0
- package/dist/skills/operate-existing-agent/SKILL.md +97 -113
- package/dist/skills/resolve-agent-decisions/SKILL.md +2 -2
- package/dist/skills/resume-agent-work/SKILL.md +19 -25
- package/dist/skills/review-agent-deliverables/SKILL.md +20 -29
- package/dist/skills/save-agent-playbook/SKILL.md +3 -4
- package/dist/src/card-output-schemas.d.ts +26 -0
- package/dist/src/card-output-schemas.js +47 -0
- package/dist/src/cards.d.ts +9 -0
- package/dist/src/cards.js +3 -0
- package/dist/src/contracts.d.ts +37 -0
- package/dist/src/directory.d.ts +2 -0
- package/dist/src/directory.js +94 -0
- package/dist/src/gtm.d.ts +2 -0
- package/dist/src/gtm.js +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/kit.d.ts +3 -0
- package/dist/src/kit.js +75 -29
- package/dist/src/package.js +6 -3
- package/dist/src/workflows.d.ts +2 -0
- package/dist/src/workflows.js +22 -4
- package/package.json +24 -5
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AgentFailure } from "./contracts.js";
|
|
2
|
+
const id = { type: 'string', pattern: '^[A-Za-z0-9_-]{1,256}$', minLength: 1, maxLength: 256 };
|
|
3
|
+
const properties = { workspaceId: id, q: { type: 'string', maxLength: 200 },
|
|
4
|
+
cursor: { type: 'string', minLength: 1, maxLength: 1536 }, limit: { type: 'integer', minimum: 1, maximum: 100 } };
|
|
5
|
+
const validId = (value) => typeof value === 'string' && /^[A-Za-z0-9_-]{1,256}$/.test(value);
|
|
6
|
+
const validDate = (value) => value === undefined || (typeof value === 'string' && Number.isFinite(Date.parse(value)));
|
|
7
|
+
const states = ['queued', 'working', 'input-required', 'succeeded', 'failed', 'cancelled', 'unknown'];
|
|
8
|
+
export function directoryTools(binding, actions, revalidate) {
|
|
9
|
+
return ['threads', 'tasks'].flatMap(kind => {
|
|
10
|
+
const action = kind === 'threads' ? 'connect' : 'task';
|
|
11
|
+
const maxLimit = kind === 'tasks' ? 10 : 100;
|
|
12
|
+
const maxCursor = kind === 'tasks' ? 1536 : 512;
|
|
13
|
+
const service = kind === 'threads' ? binding.services.listThreads : binding.services.listTasks;
|
|
14
|
+
if (!service || !actions.includes('list') || !actions.includes(action))
|
|
15
|
+
return [];
|
|
16
|
+
return [{ name: `list_my_${kind}`, description: `Choose existing ${kind} by title in an accessible workspace. Read-only; never creates or restarts work.`,
|
|
17
|
+
inputSchema: { type: 'object', properties: { ...properties, cursor: { ...properties.cursor, maxLength: maxCursor }, limit: { ...properties.limit, maximum: maxLimit }, ...(kind === 'tasks' ? { threadId: id } : {}) }, required: kind === 'threads' ? ['workspaceId'] : [], additionalProperties: false },
|
|
18
|
+
async run(args) {
|
|
19
|
+
const keys = [...Object.keys(properties), ...(kind === 'tasks' ? ['threadId'] : [])];
|
|
20
|
+
if (Object.keys(args).some(key => !keys.includes(key)) || (kind === 'threads' ? !validId(args.workspaceId) : args.workspaceId !== undefined && !validId(args.workspaceId))
|
|
21
|
+
|| (args.threadId !== undefined && !validId(args.threadId))
|
|
22
|
+
|| (args.q !== undefined && (typeof args.q !== 'string' || args.q.length > 200))
|
|
23
|
+
|| (args.cursor !== undefined && (typeof args.cursor !== 'string' || !args.cursor || args.cursor.length > maxCursor))
|
|
24
|
+
|| (args.limit !== undefined && (!Number.isSafeInteger(args.limit) || Number(args.limit) < 1 || Number(args.limit) > maxLimit)))
|
|
25
|
+
throw new AgentFailure('invalid_argument');
|
|
26
|
+
const query = args;
|
|
27
|
+
const native = async (read) => {
|
|
28
|
+
try {
|
|
29
|
+
return await read();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new AgentFailure(error instanceof AgentFailure && [401, 403, 404].includes(error.status) ? 'native_target_forbidden' : 'native_directory_unavailable', error instanceof AgentFailure && [401, 403, 404].includes(error.status) ? error.status : 503);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const visibleWorkspaces = async () => {
|
|
36
|
+
const workspaces = await native(() => binding.services.list());
|
|
37
|
+
await revalidate();
|
|
38
|
+
if (query.workspaceId && !workspaces.some(workspace => workspace.workspaceId === query.workspaceId))
|
|
39
|
+
throw new AgentFailure('native_target_forbidden', 403);
|
|
40
|
+
return new Set(workspaces.map(workspace => workspace.workspaceId));
|
|
41
|
+
};
|
|
42
|
+
const before = await visibleWorkspaces();
|
|
43
|
+
await revalidate();
|
|
44
|
+
const page = kind === 'threads' ? await native(() => binding.services.listThreads(query)) : await native(() => binding.services.listTasks(query));
|
|
45
|
+
const after = await visibleWorkspaces();
|
|
46
|
+
if (!page || typeof page !== 'object')
|
|
47
|
+
throw new AgentFailure('native_directory_invalid', 502);
|
|
48
|
+
const rows = kind === 'threads' ? ('threads' in page ? page.threads : undefined) : ('tasks' in page ? page.tasks : undefined);
|
|
49
|
+
if (!Array.isArray(rows) || rows.length > (query.limit ?? maxLimit)
|
|
50
|
+
|| (page.nextCursor !== null && (typeof page.nextCursor !== 'string' || !page.nextCursor || page.nextCursor.length > maxCursor)))
|
|
51
|
+
throw new AgentFailure('native_directory_invalid', 502);
|
|
52
|
+
const projected = [];
|
|
53
|
+
const targets = [];
|
|
54
|
+
for (const value of rows) {
|
|
55
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
56
|
+
throw new AgentFailure('native_directory_invalid', 502);
|
|
57
|
+
const row = value;
|
|
58
|
+
if (!row || !validId(row.threadId) || typeof row.title !== 'string' || row.title.length > 1024 || !validDate(row.updatedAt))
|
|
59
|
+
throw new AgentFailure('native_directory_invalid', 502);
|
|
60
|
+
const workspaceId = kind === 'tasks' ? row.workspaceId : query.workspaceId;
|
|
61
|
+
if (!validId(workspaceId) || !before.has(workspaceId) || !after.has(workspaceId) || (query.workspaceId && workspaceId !== query.workspaceId))
|
|
62
|
+
throw new AgentFailure('native_target_forbidden', 403);
|
|
63
|
+
const target = { workspaceId, threadId: row.threadId };
|
|
64
|
+
targets.push(target);
|
|
65
|
+
if (query.threadId && row.threadId !== query.threadId)
|
|
66
|
+
throw new AgentFailure('native_directory_invalid', 502);
|
|
67
|
+
await native(() => binding.authorize(target, action));
|
|
68
|
+
await revalidate();
|
|
69
|
+
const summary = { threadId: row.threadId, title: row.title, ...(row.updatedAt ? { updatedAt: row.updatedAt } : {}) };
|
|
70
|
+
if (kind === 'tasks') {
|
|
71
|
+
if (!validId(row.turnId) || (typeof row.state !== 'string' || !states.includes(row.state)) || !validDate(row.completedAt) || typeof row.completionVerified !== 'boolean')
|
|
72
|
+
throw new AgentFailure('native_directory_invalid', 502);
|
|
73
|
+
for (const name of [row.workspaceName, row.threadTitle])
|
|
74
|
+
if (name !== undefined && (typeof name !== 'string' || name.length > 1024))
|
|
75
|
+
throw new AgentFailure('native_directory_invalid', 502);
|
|
76
|
+
if (!validDate(row.createdAt))
|
|
77
|
+
throw new AgentFailure('native_directory_invalid', 502);
|
|
78
|
+
projected.push({ ...summary, ...target, turnId: row.turnId, state: row.state, completionVerified: row.completionVerified === true, ...(row.statusError ? { statusError: 'status_unavailable' } : {}), ...(row.workspaceName ? { workspaceName: row.workspaceName } : {}), ...(row.threadTitle ? { threadTitle: row.threadTitle } : {}), ...(row.createdAt ? { createdAt: row.createdAt } : {}), ...(row.completedAt ? { completedAt: row.completedAt } : {}) });
|
|
79
|
+
}
|
|
80
|
+
else if (kind === 'threads')
|
|
81
|
+
projected.push(summary);
|
|
82
|
+
else
|
|
83
|
+
throw new AgentFailure('native_directory_invalid', 502);
|
|
84
|
+
}
|
|
85
|
+
// Authorization can change while later rows await native services.
|
|
86
|
+
await visibleWorkspaces();
|
|
87
|
+
for (const target of targets)
|
|
88
|
+
await native(() => binding.authorize(target, action));
|
|
89
|
+
await revalidate();
|
|
90
|
+
return { appName: binding.appName, identityId: binding.applicationUserId, ...(query.workspaceId ? { workspaceId: query.workspaceId } : {}),
|
|
91
|
+
...(query.threadId ? { threadId: query.threadId } : {}), [kind]: projected, nextCursor: page.nextCursor };
|
|
92
|
+
} }];
|
|
93
|
+
});
|
|
94
|
+
}
|
package/dist/src/gtm.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface GtmNativeContext {
|
|
|
13
13
|
/** Use the persisted agent-app turn path. HTTP 200 alone is NOT admission. */
|
|
14
14
|
turns?: Pick<NativeServices, 'startTask' | 'getTask'>;
|
|
15
15
|
interactions?: Pick<NativeServices, 'pendingDecisions' | 'decide'>;
|
|
16
|
+
/** Native paginated discovery; absent services expose no discovery tools. */
|
|
17
|
+
directory?: Pick<NativeServices, 'listThreads' | 'listTasks'>;
|
|
16
18
|
/** Only supply a live SAME-workspace application-line adapter, not createHostedAgent's legacy per-person sandbox path. */
|
|
17
19
|
channels?: Pick<NativeServices, 'channelBinding' | 'sendChannel'>;
|
|
18
20
|
}
|
package/dist/src/gtm.js
CHANGED
|
@@ -69,6 +69,7 @@ export function createGtmBinding(context) {
|
|
|
69
69
|
throw new AgentFailure('gtm_binary_output_unsupported', 415);
|
|
70
70
|
return { content: result.file.content, revision, mediaType };
|
|
71
71
|
},
|
|
72
|
+
...context.directory,
|
|
72
73
|
...context.turns,
|
|
73
74
|
...context.interactions,
|
|
74
75
|
...context.channels,
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createAgentsHandler } from './kit.ts';
|
|
2
2
|
export { createGtmBinding } from './gtm.ts';
|
|
3
3
|
export { AgentFailure } from './contracts.ts';
|
|
4
|
-
export type { Action, Metadata, NativeBinding, NativeChannel, NativeFile, NativeMcpFactory, NativeServices, NativeTask, NativeResponse, NativeConnection, NativeEnvironment, OAuthResource, Principal, Target, } from './contracts.ts';
|
|
4
|
+
export type { Action, Metadata, NativeBinding, NativeChannel, NativeFile, NativeMcpFactory, NativeServices, NativeTask, NativeResponse, NativeConnection, NativeEnvironment, OAuthResource, Principal, Target, NativeDirectoryQuery, NativeTaskQuery, NativeThreadSummary, NativeTaskSummary, } from './contracts.ts';
|
|
5
5
|
export type { GtmNativeContext } from './gtm.ts';
|
|
6
6
|
export type { InspectionBinding, InspectionExposure } from './inspection.ts';
|
|
7
7
|
export { createTaskEvents, TASK_EVENT } from './task-events.ts';
|
package/dist/src/kit.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AgentCardsResource } from './cards.ts';
|
|
1
2
|
import { type AgentAppDescription } from './app-metadata.ts';
|
|
2
3
|
import type { NativeBinding, NativeMcpFactory, OAuthResource, Principal } from './contracts.ts';
|
|
3
4
|
import type { InspectionBinding } from './inspection.ts';
|
|
@@ -17,5 +18,7 @@ export declare function createAgentsHandler(options: {
|
|
|
17
18
|
taskWait?: TaskWaitPolicy;
|
|
18
19
|
/** Optional durable event adapter, bound to this host's native grant store and worker. */
|
|
19
20
|
events?: TaskEvents;
|
|
21
|
+
/** Optional MCP Apps renderer. Omit to retain the tools-only protocol. */
|
|
22
|
+
cards?: AgentCardsResource;
|
|
20
23
|
allowedOrigins?: readonly string[];
|
|
21
24
|
}): (request: Request) => Promise<Response>;
|
package/dist/src/kit.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { AGENT_CARDS_URI, AGENT_CARDS_MIME, AGENT_CARD_TOOLS } from "./cards.js";
|
|
2
|
+
import { directoryTools } from "./directory.js";
|
|
3
|
+
import { cardOutputSchemas } from "./card-output-schemas.js";
|
|
1
4
|
import { agentAppDiscovery, defineAgentAppMetadata } from "./app-metadata.js";
|
|
2
5
|
import { AgentFailure } from "./contracts.js";
|
|
3
6
|
import { ACTION_TOOLS as names, HANDOFF_ACTIONS, describeAgentWorkflows } from "./workflows.js";
|
|
@@ -6,6 +9,12 @@ import { observeTask, taskWaitPolicy, validateNativeTask } from "./task-wait.js"
|
|
|
6
9
|
import { describeConnection, digest, projectEnvironment } from "./connection.js";
|
|
7
10
|
export { digest } from "./connection.js";
|
|
8
11
|
const failureMarker = Symbol('native-tool-failure');
|
|
12
|
+
/** Only kit checks completed before native admission may permit reconnect and retry. */
|
|
13
|
+
class PreAdmissionFailure extends AgentFailure {
|
|
14
|
+
constructor(error) {
|
|
15
|
+
super(error instanceof AgentFailure ? error.message : 'native_operation_failed', error instanceof AgentFailure ? error.status : 503);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
9
18
|
/** An error after entering native admission must retain its reconciliation handle. */
|
|
10
19
|
class AdmissionFailure extends AgentFailure {
|
|
11
20
|
recovery;
|
|
@@ -98,7 +107,8 @@ async function output(binding, target, path) {
|
|
|
98
107
|
async function taskReceipt(binding, target, task, expectedTurn, canReadOutput, checkOutput) {
|
|
99
108
|
validateNativeTask(task, target, expectedTurn);
|
|
100
109
|
const base = { ...target, turnId: task.turnId, executionId: task.executionId, state: task.state, source: task.source, ...(task.environment ? { environment: projectEnvironment(task.environment) } : {}) };
|
|
101
|
-
|
|
110
|
+
const settledResponse = ['failed', 'input-required'].includes(task.state) && task.response && task.completedAt;
|
|
111
|
+
if (task.state !== 'succeeded' && !settledResponse)
|
|
102
112
|
return { ...base, completionVerified: false };
|
|
103
113
|
if (!task.executionId || !task.completedAt || !Number.isFinite(Date.parse(task.completedAt)) || (!task.outputs?.length && !task.response))
|
|
104
114
|
return { ...base, completionVerified: false, reason: 'execution_output_provenance_missing' };
|
|
@@ -125,18 +135,24 @@ async function taskReceipt(binding, target, task, expectedTurn, canReadOutput, c
|
|
|
125
135
|
if (bytes > 262144)
|
|
126
136
|
throw new AgentFailure('output_too_large', 413);
|
|
127
137
|
outcome = value.outcome ?? 'completed';
|
|
138
|
+
if (task.state !== 'succeeded' && outcome !== task.state)
|
|
139
|
+
throw new AgentFailure('native_response_outcome_mismatch', 502);
|
|
128
140
|
// Explicit projection: do not expose arbitrary host/provider fields.
|
|
129
141
|
response = { content: value.content, mediaType: value.mediaType, sha256: await digest(value.content), bytes };
|
|
130
142
|
}
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
const unavailableOutput = (reason) => ({ ...base, completionVerified: false, reason,
|
|
144
|
+
...(outcome !== 'completed' && response ? { state: outcome, response, completedAt: task.completedAt, outputs: [] } : {}) });
|
|
145
|
+
if (task.outputs?.length && !binding.services.readFile) {
|
|
146
|
+
await checkOutput();
|
|
147
|
+
return unavailableOutput('output_reader_unavailable');
|
|
148
|
+
}
|
|
133
149
|
const outputs = [];
|
|
134
150
|
for (const ref of task.outputs ?? []) {
|
|
135
151
|
await checkOutput();
|
|
136
152
|
const file = await output(binding, target, ref.path);
|
|
137
153
|
await checkOutput();
|
|
138
154
|
if (!ref.revision || file.revision !== ref.revision)
|
|
139
|
-
return
|
|
155
|
+
return unavailableOutput('output_revision_changed');
|
|
140
156
|
outputs.push(file);
|
|
141
157
|
}
|
|
142
158
|
return { ...base, ...(outcome !== 'completed' ? { state: outcome, reason: 'native_result_not_completed' } : {}),
|
|
@@ -184,24 +200,29 @@ async function run(action, args, binding, revalidate, signal, policy, canReadTas
|
|
|
184
200
|
break;
|
|
185
201
|
}
|
|
186
202
|
case 'delegate': {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
203
|
+
try {
|
|
204
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(args.turnId))
|
|
205
|
+
throw new AgentFailure('turn_id_must_be_uuid');
|
|
206
|
+
// Validate observation authorization BEFORE admitting work. A denied wait must not create a task.
|
|
207
|
+
if (Math.min(args.waitMs ?? policy.defaultMs, policy.maxMs) > 0) {
|
|
208
|
+
if (!canReadTask)
|
|
209
|
+
throw new AgentFailure('task_read_scope_required', 403);
|
|
210
|
+
await binding.authorize(target, 'task');
|
|
211
|
+
await revalidate();
|
|
212
|
+
}
|
|
213
|
+
if (args.contextVersion !== undefined) {
|
|
214
|
+
if (!canConnect)
|
|
215
|
+
throw new AgentFailure('connect_scope_required', 403);
|
|
216
|
+
await binding.authorize(target, 'connect');
|
|
217
|
+
await revalidate();
|
|
218
|
+
if ((await describeConnection(await s.connect(target))).contextVersion !== args.contextVersion)
|
|
219
|
+
throw new AgentFailure('agent_context_changed_reconnect_required', 409);
|
|
220
|
+
await binding.authorize(target, 'delegate');
|
|
221
|
+
await revalidate();
|
|
222
|
+
}
|
|
195
223
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
throw new AgentFailure('connect_scope_required', 403);
|
|
199
|
-
await binding.authorize(target, 'connect');
|
|
200
|
-
await revalidate();
|
|
201
|
-
if ((await describeConnection(await s.connect(target))).contextVersion !== args.contextVersion)
|
|
202
|
-
throw new AgentFailure('agent_context_changed_reconnect_required', 409);
|
|
203
|
-
await binding.authorize(target, 'delegate');
|
|
204
|
-
await revalidate();
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw new PreAdmissionFailure(error);
|
|
205
226
|
}
|
|
206
227
|
try {
|
|
207
228
|
const task = await s.startTask(target, args.turnId, args.prompt, args.contextVersion === undefined ? undefined : { contextVersion: args.contextVersion });
|
|
@@ -422,7 +443,7 @@ export function createAgentsHandler(options) {
|
|
|
422
443
|
|| (typeof params.name === 'string' && request.headers.get('mcp-name') !== params.name))
|
|
423
444
|
return rpcResult({ error: { code: -32600, message: 'MCP routing headers do not match request' } });
|
|
424
445
|
if (parsed.method === 'server/discover')
|
|
425
|
-
return rpcResult({ result: { resultType: 'complete', supportedVersions: ['2026-07-28'], serverInfo: { name: metadata.name, version: '0.1.0' }, capabilities: { tools: {}, ...(eventsEnabled ? { events: {} } : {}) } } });
|
|
446
|
+
return rpcResult({ result: { resultType: 'complete', supportedVersions: ['2026-07-28'], serverInfo: { name: metadata.name, version: '0.1.0' }, capabilities: { tools: {}, ...(options.cards ? { resources: {} } : {}), ...(eventsEnabled ? { events: {} } : {}) } } });
|
|
426
447
|
}
|
|
427
448
|
if (parsed.method.startsWith('events/')) {
|
|
428
449
|
if (!v2 || !eventsEnabled)
|
|
@@ -436,14 +457,31 @@ export function createAgentsHandler(options) {
|
|
|
436
457
|
}
|
|
437
458
|
return rpcResult(result);
|
|
438
459
|
}
|
|
460
|
+
if (options.cards && (parsed.method === 'resources/list' || parsed.method === 'resources/read')) {
|
|
461
|
+
const descriptor = { uri: AGENT_CARDS_URI, name: 'agent-cards', title: `${metadata.displayName} agents`, mimeType: AGENT_CARDS_MIME };
|
|
462
|
+
if (parsed.method === 'resources/list')
|
|
463
|
+
return rpcResult({ result: { resources: [descriptor], ...(v2 ? { resultType: 'complete' } : {}) } });
|
|
464
|
+
if (!record(parsed.params) || parsed.params.uri !== AGENT_CARDS_URI)
|
|
465
|
+
return rpcResult({ error: { code: -32002, message: 'Resource not found' } });
|
|
466
|
+
return rpcResult({ result: { contents: [{ ...descriptor, text: options.cards.html,
|
|
467
|
+
_meta: { ui: { prefersBorder: true, csp: { connectDomains: [], resourceDomains: [] }, ...(options.cards.domain ? { domain: options.cards.domain } : {}) } },
|
|
468
|
+
}], ...(v2 ? { resultType: 'complete' } : {}) } });
|
|
469
|
+
}
|
|
470
|
+
const directory = directoryTools(binding, actions, revalidate);
|
|
471
|
+
const executableTools = [...actions.map(action => names[action]), ...directory.map(tool => tool.name)];
|
|
439
472
|
const workflows = describeAgentWorkflows(actions);
|
|
440
473
|
const handoff = workflows.some(workflow => workflow.id === 'handoff-to-agent') ? createHandoffTool(binding, revalidate) : null;
|
|
441
474
|
const profileSchema = { type: 'object', properties: { id: { type: 'string', minLength: 1 }, name: { type: 'string' } }, required: ['id'], additionalProperties: false };
|
|
442
475
|
const tools = [
|
|
443
476
|
{ name: 'agent_profile', description: 'Stable application identity of the authenticated account.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, run: async (args) => { validate(args, {}); return { id: binding.applicationUserId, ...(binding.name ? { name: binding.name } : {}) }; } },
|
|
444
|
-
{ name: 'agent_capabilities', description: 'Discover actions actually supported for this identity, app and grant now.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, run: async (args) => { validate(args, {}); return { appName: binding.appName, actions: [...
|
|
477
|
+
{ name: 'agent_capabilities', description: 'Discover actions actually supported for this identity, app and grant now.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, run: async (args) => { validate(args, {}); return { appName: binding.appName, actions: [...executableTools, ...(handoff ? [handoff.name] : [])], workflows, inspection: inspection?.tools.map(tool => tool.name) ?? [], media: ['text/plain; charset=utf-8'], compatibilityAliases: actions.includes('delegate') ? { delegate_task: names.delegate } : {}, preferredPromptTool: actions.includes('delegate') ? names.delegate : null, resultKinds: canReadOutput ? [...(binding.services.readFile ? ['files'] : []), ...(binding.services.readResponse ? ['response'] : [])] : [], execution: 'native-application', stateStore: 'native-application', taskWait: { defaultMs: waitPolicy.defaultMs, maxMs: waitPolicy.maxMs }, events: eventsEnabled ? { name: 'agent.task_updated', protocolVersion: '2026-07-28', delivery: ['webhook'], replay: false } : null }; } },
|
|
445
478
|
...advertisedActions.map(action => ({ name: names[action], description: descriptions[action], inputSchema: { type: 'object', properties: Object.fromEntries(Object.entries(schemas[action]).map(([key, value]) => { const { optional: _optional, ...schema } = value; return [key, schema]; })), required: Object.keys(schemas[action]).filter(key => !schemas[action][key].optional), additionalProperties: false }, run: async (args) => {
|
|
446
|
-
|
|
479
|
+
try {
|
|
480
|
+
validate(args, schemas[action]);
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
throw action === 'delegate' ? new PreAdmissionFailure(error) : error;
|
|
484
|
+
}
|
|
447
485
|
if (!actions.includes(action))
|
|
448
486
|
throw new AgentFailure('insufficient_scope', 403);
|
|
449
487
|
if (action === 'delegate' && args.contextVersion !== undefined && !actions.includes('connect'))
|
|
@@ -453,6 +491,7 @@ export function createAgentsHandler(options) {
|
|
|
453
491
|
? { ...result, notification: { state: 'not_subscribed', name: 'agent.task_updated', arguments: { workspaceId: args.workspaceId, threadId: args.threadId, turnId: args.turnId } } }
|
|
454
492
|
: result;
|
|
455
493
|
} })),
|
|
494
|
+
...directory,
|
|
456
495
|
...(handoff ? [handoff] : []),
|
|
457
496
|
...(inspection?.tools ?? []),
|
|
458
497
|
];
|
|
@@ -469,14 +508,14 @@ export function createAgentsHandler(options) {
|
|
|
469
508
|
return await tool.run(args, {});
|
|
470
509
|
}
|
|
471
510
|
catch (error) {
|
|
472
|
-
return { [failureMarker]: { code: error instanceof AgentFailure ? error.message : 'native_operation_failed', status: error instanceof AgentFailure ? error.status : 503, ...(error instanceof AdmissionFailure ? { recovery: error.recovery } : {}) } };
|
|
511
|
+
return { [failureMarker]: { code: error instanceof AgentFailure ? error.message : 'native_operation_failed', status: error instanceof AgentFailure ? error.status : 503, ...(error instanceof PreAdmissionFailure ? { admission: 'not-started' } : {}), ...(error instanceof AdmissionFailure ? { recovery: error.recovery } : {}) } };
|
|
473
512
|
} } })),
|
|
474
513
|
buildEnv: () => ({}),
|
|
475
|
-
formatResult: result => {
|
|
514
|
+
formatResult: (result, tool) => {
|
|
476
515
|
const failed = result && typeof result === 'object' && failureMarker in result ? result[failureMarker] : null;
|
|
477
516
|
return failed
|
|
478
517
|
? { isError: true, content: [{ type: 'text', text: failed.code }], structuredContent: { error: failed } }
|
|
479
|
-
: { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result };
|
|
518
|
+
: { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result, ...(options.cards && AGENT_CARD_TOOLS.includes(tool.name) ? { _meta: { agentCards: { displayName: metadata.displayName, applicationOrigin: metadata.applicationOrigin, tools: executableTools } } } : {}) };
|
|
480
519
|
},
|
|
481
520
|
});
|
|
482
521
|
if (parsed.method === 'tools/call' && record(parsed.params) && parsed.params.name === 'delegate_task') {
|
|
@@ -498,8 +537,9 @@ export function createAgentsHandler(options) {
|
|
|
498
537
|
const handoffScopes = tool.name === HANDOFF_TOOL_NAME
|
|
499
538
|
? [...new Set(HANDOFF_ACTIONS.flatMap(a => binding.requiredScopes[a] ?? []))]
|
|
500
539
|
: [];
|
|
501
|
-
const
|
|
502
|
-
|
|
540
|
+
const directoryScopes = directory.some(item => item.name === tool.name) ? [...new Set([...(binding.requiredScopes.list ?? []), ...(binding.requiredScopes[tool.name === 'list_my_threads' ? 'connect' : 'task'] ?? [])])] : null;
|
|
541
|
+
const securitySchemes = [{ type: 'oauth2', scopes: directoryScopes ?? (inspectionTool ? inspectionScopes : action ? binding.requiredScopes[action] : handoffScopes) }];
|
|
542
|
+
Object.assign(tool, { annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: readOnly, openWorldHint: action === 'delegate' || action === 'decide' || action === 'channel' }, securitySchemes, _meta: { securitySchemes, ...(tool.name === 'agent_profile' ? { 'openai/profile': true } : {}), ...(options.cards && AGENT_CARD_TOOLS.includes(String(tool.name)) ? { ui: { resourceUri: AGENT_CARDS_URI, visibility: ['model', 'app'] } } : {}) }, ...(tool.name === 'agent_profile' ? { outputSchema: profileSchema } : {}), ...(Object.hasOwn(cardOutputSchemas, String(tool.name)) ? { outputSchema: cardOutputSchemas[tool.name] } : {}) });
|
|
503
543
|
}
|
|
504
544
|
if (v2 && envelope.result) {
|
|
505
545
|
envelope.result.tools?.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
@@ -507,6 +547,12 @@ export function createAgentsHandler(options) {
|
|
|
507
547
|
}
|
|
508
548
|
return Response.json(envelope, { headers: { 'cache-control': 'no-store' } });
|
|
509
549
|
}
|
|
550
|
+
if (options.cards && parsed.method === 'initialize' && response.ok) {
|
|
551
|
+
const envelope = await response.json();
|
|
552
|
+
if (envelope.result)
|
|
553
|
+
envelope.result.capabilities = { ...envelope.result.capabilities, resources: {} };
|
|
554
|
+
return Response.json(envelope, { headers: { 'cache-control': 'no-store' } });
|
|
555
|
+
}
|
|
510
556
|
if (v2 && response.ok && response.status !== 204) {
|
|
511
557
|
const envelope = await response.json();
|
|
512
558
|
if (envelope.result)
|
package/dist/src/package.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { AGENT_APP_METADATA_KEY } from "./app-metadata.js";
|
|
6
|
-
import { ACTION_TOOLS, selectAgentWorkflows, describeAgentWorkflows } from "./workflows.js";
|
|
6
|
+
import { ACTION_TOOLS, selectAgentWorkflows, describeAgentWorkflows, selectAgentStarterPrompts } from "./workflows.js";
|
|
7
7
|
export function endpointUrl(value, allowLoopback = false) {
|
|
8
8
|
const url = new URL(value);
|
|
9
9
|
if (url.username || url.password || url.search || url.hash || (url.protocol !== 'https:' && !(allowLoopback && url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))))
|
|
@@ -159,7 +159,7 @@ async function writePackage(m, url, out, discovered, connectionId) {
|
|
|
159
159
|
await mkdir(join(temporary, 'skills/operate-existing-agent'), { recursive: true });
|
|
160
160
|
await writeFile(join(temporary, 'plugin.json'), JSON.stringify({
|
|
161
161
|
$schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', name: m.name, version: '0.1.0', description: m.description,
|
|
162
|
-
extensions: { 'com.openai': { ...(connectionId ? { apps: './.app.json' } : {}), interface: { displayName: m.displayName, shortDescription: m.description, developerName: 'Tangle', category: 'Productivity', websiteURL: m.applicationOrigin, defaultPrompt:
|
|
162
|
+
extensions: { 'com.openai': { ...(connectionId ? { apps: './.app.json' } : {}), interface: { displayName: m.displayName, shortDescription: m.description, developerName: 'Tangle', category: 'Productivity', websiteURL: m.applicationOrigin, defaultPrompt: selectAgentStarterPrompts(m.actions) } } },
|
|
163
163
|
}, null, 2) + '\n');
|
|
164
164
|
if (connectionId)
|
|
165
165
|
await writeFile(join(temporary, '.app.json'), JSON.stringify({ apps: { agents: { id: connectionId } } }, null, 2) + '\n');
|
|
@@ -188,7 +188,10 @@ async function writePackage(m, url, out, discovered, connectionId) {
|
|
|
188
188
|
? `4. Verify that .app.json maps this endpoint's registered connection: ${connectionId}. This mapping is developer-supplied, not verified by the generator.`
|
|
189
189
|
: '4. Copy the real registered connection ID from ChatGPT. Rerun the installed generator with --connection-id and a new --out directory to add its .app.json mapping.',
|
|
190
190
|
'5. Import/install that generated package using the current ChatGPT plugin instructions; test in a new chat.',
|
|
191
|
-
'6. Ask
|
|
191
|
+
'6. Ask for the agent or project by name. Choose the existing conversation when several matches appear.', '',
|
|
192
|
+
'Describe the result you want, review it here, then ask for a specific change in the same conversation.',
|
|
193
|
+
'Return later by asking for that work by name. Available discovery tools determine which work ChatGPT can find.', '',
|
|
194
|
+
'## Developer acceptance', '',
|
|
192
195
|
'Prompt the selected agent; use get_task to observe the same turn.',
|
|
193
196
|
'Reopen results with read_output and the returned native references.',
|
|
194
197
|
'A queued receipt is not completion. Missing capabilities stay unavailable.', '',
|
package/dist/src/workflows.d.ts
CHANGED
|
@@ -17,3 +17,5 @@ export declare function describeAgentWorkflows(actions: readonly Action[]): {
|
|
|
17
17
|
prompt: string;
|
|
18
18
|
requiredTools: string[];
|
|
19
19
|
}[];
|
|
20
|
+
/** Starters describe only packaged actions; live discovery still decides what can run. */
|
|
21
|
+
export declare function selectAgentStarterPrompts(actions: readonly Action[]): string[];
|
package/dist/src/workflows.js
CHANGED
|
@@ -8,16 +8,16 @@ export const HANDOFF_ACTIONS = ['connect', 'handoff', 'output', 'delegate', 'tas
|
|
|
8
8
|
/** These compose existing actions. They neither register tools nor grant access. */
|
|
9
9
|
export const AGENT_WORKFLOWS = [
|
|
10
10
|
{ id: 'handoff-to-agent', title: 'Hand this work to my agent',
|
|
11
|
-
prompt: '
|
|
11
|
+
prompt: 'Help my agent finish this work using the context we have chosen.',
|
|
12
12
|
requires: HANDOFF_ACTIONS },
|
|
13
13
|
{ id: 'review-agent-deliverables', title: 'Review and improve my agent’s work',
|
|
14
|
-
prompt: '
|
|
14
|
+
prompt: 'Review my agent’s latest result and help me improve it.',
|
|
15
15
|
requires: ['connect', 'task', 'delegate'] },
|
|
16
16
|
{ id: 'save-agent-playbook', title: 'Save this workflow for my agent',
|
|
17
|
-
prompt: '
|
|
17
|
+
prompt: 'Save how we did this as a playbook for my agent.',
|
|
18
18
|
requires: ['connect', 'handoff', 'output'] },
|
|
19
19
|
{ id: 'resume-agent-work', title: 'Pick up where my agent left off',
|
|
20
|
-
prompt: '
|
|
20
|
+
prompt: 'Pick up my recent work and show what needs my attention.',
|
|
21
21
|
requires: ['connect', 'task'] },
|
|
22
22
|
{ id: 'resolve-agent-decisions', title: 'Resolve my agent’s pending decisions',
|
|
23
23
|
prompt: 'Show the pending decisions for my selected agent so I can review them.',
|
|
@@ -36,3 +36,21 @@ export function describeAgentWorkflows(actions) {
|
|
|
36
36
|
id, title, prompt, requiredTools: requires.map(action => ACTION_TOOLS[action]),
|
|
37
37
|
}));
|
|
38
38
|
}
|
|
39
|
+
/** Starters describe only packaged actions; live discovery still decides what can run. */
|
|
40
|
+
export function selectAgentStarterPrompts(actions) {
|
|
41
|
+
const available = new Set(actions);
|
|
42
|
+
const prompts = [];
|
|
43
|
+
if (available.has('list'))
|
|
44
|
+
prompts.push('Show my agents and help me choose what to work on.');
|
|
45
|
+
if (available.has('connect') && available.has('delegate')) {
|
|
46
|
+
prompts.push('Help my agent finish a task in its existing workspace.');
|
|
47
|
+
}
|
|
48
|
+
const workflows = selectAgentWorkflows(actions);
|
|
49
|
+
const resume = workflows.find(workflow => workflow.id === 'resume-agent-work');
|
|
50
|
+
if (resume)
|
|
51
|
+
prompts.push(resume.prompt);
|
|
52
|
+
for (const workflow of workflows)
|
|
53
|
+
if (!prompts.includes(workflow.prompt))
|
|
54
|
+
prompts.push(workflow.prompt);
|
|
55
|
+
return prompts.length ? prompts.slice(0, 3) : ['Show what this agent connection can do.'];
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/chatgpt-agents-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/tangle-network/chatgpt-plugins.git",
|
|
@@ -28,21 +28,29 @@
|
|
|
28
28
|
"./events-node": {
|
|
29
29
|
"types": "./dist/src/events-node.d.ts",
|
|
30
30
|
"import": "./dist/src/events-node.js"
|
|
31
|
+
},
|
|
32
|
+
"./cards": {
|
|
33
|
+
"types": "./dist/cards/server.d.ts",
|
|
34
|
+
"import": "./dist/cards/server.js"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"files": [
|
|
34
38
|
"dist",
|
|
35
39
|
"README.md",
|
|
36
|
-
"SETUP.md"
|
|
40
|
+
"SETUP.md",
|
|
41
|
+
"CARDS.md"
|
|
37
42
|
],
|
|
38
43
|
"scripts": {
|
|
39
44
|
"check": "tsc --noEmit",
|
|
40
|
-
"build": "npm run check && tsc -p tsconfig.build.json && node scripts/build-assets.mjs",
|
|
41
|
-
"test": "node --experimental-strip-types --test tests/kit.test.ts tests/workflows.test.ts tests/task-turn.test.ts tests/persistent-agent.test.ts tests/distribution.test.ts tests/setup.test.ts && npm run test:package",
|
|
45
|
+
"build": "npm run check && tsc -p tsconfig.build.json && node scripts/build-assets.mjs && node scripts/build-cards.mjs",
|
|
46
|
+
"test": "node --experimental-strip-types --test tests/kit.test.ts tests/workflows.test.ts tests/task-turn.test.ts tests/persistent-agent.test.ts tests/distribution.test.ts tests/setup.test.ts tests/cards.test.ts && npm run test:package",
|
|
42
47
|
"test:package": "node --experimental-strip-types --test tests/packaged-generator.test.mjs",
|
|
43
48
|
"acceptance": "node --experimental-strip-types acceptance/run.ts",
|
|
44
49
|
"build:plugin": "node dist/src/cli.js",
|
|
45
|
-
"verify:turn": "npm run build && node scripts/prove-turn.mjs"
|
|
50
|
+
"verify:turn": "npm run build && node scripts/prove-turn.mjs",
|
|
51
|
+
"test:cards": "node --experimental-strip-types --test tests/cards.test.ts",
|
|
52
|
+
"proof:cards": "node cards/proof.mjs",
|
|
53
|
+
"proof:cards:lifecycle": "node cards/proof-lifecycle.mjs"
|
|
46
54
|
},
|
|
47
55
|
"dependencies": {
|
|
48
56
|
"@modelcontextprotocol/sdk": "1.31.0",
|
|
@@ -51,8 +59,19 @@
|
|
|
51
59
|
"zod": "4.6.5"
|
|
52
60
|
},
|
|
53
61
|
"devDependencies": {
|
|
62
|
+
"@axe-core/playwright": "4.11.1",
|
|
63
|
+
"@modelcontextprotocol/ext-apps": "2.0.3",
|
|
64
|
+
"@playwright/test": "1.58.2",
|
|
54
65
|
"@tangle-network/agent-app": "0.50.11",
|
|
66
|
+
"@tangle-network/brand": "1.10.0",
|
|
67
|
+
"@tangle-network/ui": "11.13.0",
|
|
55
68
|
"@types/node": "22.15.30",
|
|
69
|
+
"@types/react": "19.2.14",
|
|
70
|
+
"@types/react-dom": "19.2.3",
|
|
71
|
+
"ajv": "8.18.0",
|
|
72
|
+
"esbuild": "0.27.4",
|
|
73
|
+
"react": "19.2.4",
|
|
74
|
+
"react-dom": "19.2.4",
|
|
56
75
|
"typescript": "5.9.3"
|
|
57
76
|
}
|
|
58
77
|
}
|