@modelprofile.com/flexharness 10.17.0 → 10.18.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/errors.d.ts +4 -0
- package/dist_ts/errors.js +9 -1
- package/dist_ts/functions.toolpermissions.d.ts +24 -0
- package/dist_ts/functions.toolpermissions.js +38 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/plugins.d.ts +1 -1
- package/dist_ts/plugins.js +1 -1
- package/dist_ts_chat_harness/classes.chatclient.d.ts +95 -0
- package/dist_ts_chat_harness/classes.chatclient.js +263 -0
- package/dist_ts_chat_harness/classes.chatrequesterror.d.ts +4 -0
- package/dist_ts_chat_harness/classes.chatrequesterror.js +8 -0
- package/dist_ts_chat_harness/classes.transcript.d.ts +1 -1
- package/dist_ts_chat_harness/classes.transcript.js +1 -1
- package/dist_ts_chat_harness/functions.intents.d.ts +1 -2
- package/dist_ts_chat_harness/functions.intents.js +1 -1
- package/dist_ts_chat_harness/functions.mapping.d.ts +1 -1
- package/dist_ts_chat_harness/functions.mapping.js +1 -1
- package/dist_ts_chat_harness/index.d.ts +3 -0
- package/dist_ts_chat_harness/index.js +3 -1
- package/dist_ts_chat_harness/interfaces.chat.d.ts +136 -0
- package/dist_ts_chat_harness/interfaces.chat.js +2 -0
- package/dist_ts_chat_harness/plugins.js +1 -1
- package/dist_ts_chat_harness_server/classes.chathub.d.ts +78 -0
- package/dist_ts_chat_harness_server/classes.chathub.js +332 -0
- package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +55 -0
- package/dist_ts_chat_harness_server/classes.chatsubscription.js +236 -0
- package/dist_ts_chat_harness_server/classes.sessionlists.d.ts +21 -0
- package/dist_ts_chat_harness_server/classes.sessionlists.js +70 -0
- package/dist_ts_chat_harness_server/classes.turngate.d.ts +38 -0
- package/dist_ts_chat_harness_server/classes.turngate.js +207 -0
- package/dist_ts_chat_harness_server/errors.d.ts +5 -0
- package/dist_ts_chat_harness_server/errors.js +9 -0
- package/dist_ts_chat_harness_server/functions.errorprojector.d.ts +11 -0
- package/dist_ts_chat_harness_server/functions.errorprojector.js +32 -0
- package/dist_ts_chat_harness_server/functions.validation.d.ts +18 -0
- package/dist_ts_chat_harness_server/functions.validation.js +88 -0
- package/dist_ts_chat_harness_server/index.d.ts +4 -0
- package/dist_ts_chat_harness_server/index.js +4 -0
- package/dist_ts_chat_harness_server/interfaces.d.ts +67 -0
- package/dist_ts_chat_harness_server/interfaces.js +2 -0
- package/dist_ts_chat_harness_server/plugins.d.ts +7 -0
- package/dist_ts_chat_harness_server/plugins.js +6 -0
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.d.ts +22 -0
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.js +55 -0
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.d.ts +46 -0
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.js +110 -0
- package/dist_ts_chat_harness_typedsocket/index.d.ts +3 -0
- package/dist_ts_chat_harness_typedsocket/index.js +3 -0
- package/dist_ts_chat_harness_typedsocket/interfaces.requests.d.ts +38 -0
- package/dist_ts_chat_harness_typedsocket/interfaces.requests.js +2 -0
- package/dist_ts_chat_harness_typedsocket/plugins.d.ts +9 -0
- package/dist_ts_chat_harness_typedsocket/plugins.js +8 -0
- package/dist_ts_tools/tool.filesystem.d.ts +2 -0
- package/dist_ts_tools/tool.filesystem.js +42 -1
- package/dist_ts_tools_node/classes.workspacereversion.d.ts +48 -0
- package/dist_ts_tools_node/classes.workspacereversion.js +202 -0
- package/dist_ts_tools_node/functions.fileimages.d.ts +22 -0
- package/dist_ts_tools_node/functions.fileimages.js +87 -0
- package/dist_ts_tools_node/index.d.ts +3 -1
- package/dist_ts_tools_node/index.js +2 -1
- package/dist_ts_tools_node/plugins.d.ts +1 -0
- package/dist_ts_tools_node/tool.context.d.ts +20 -0
- package/dist_ts_tools_node/tool.context.js +25 -4
- package/package.json +28 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/errors.ts +10 -0
- package/ts/functions.toolpermissions.ts +60 -0
- package/ts/index.ts +6 -0
- package/ts/plugins.ts +1 -0
- package/ts_chat_harness/classes.chatclient.ts +343 -0
- package/ts_chat_harness/classes.chatrequesterror.ts +7 -0
- package/ts_chat_harness/classes.transcript.ts +1 -1
- package/ts_chat_harness/functions.intents.ts +2 -2
- package/ts_chat_harness/functions.mapping.ts +1 -1
- package/ts_chat_harness/index.ts +20 -0
- package/ts_chat_harness/interfaces.chat.ts +108 -0
- package/ts_chat_harness/readme.md +24 -0
- package/ts_chat_harness_server/classes.chathub.ts +400 -0
- package/ts_chat_harness_server/classes.chatsubscription.ts +257 -0
- package/ts_chat_harness_server/classes.sessionlists.ts +90 -0
- package/ts_chat_harness_server/classes.turngate.ts +247 -0
- package/ts_chat_harness_server/errors.ts +9 -0
- package/ts_chat_harness_server/functions.errorprojector.ts +40 -0
- package/ts_chat_harness_server/functions.validation.ts +104 -0
- package/ts_chat_harness_server/index.ts +19 -0
- package/ts_chat_harness_server/interfaces.ts +92 -0
- package/ts_chat_harness_server/plugins.ts +62 -0
- package/ts_chat_harness_server/readme.md +91 -0
- package/ts_chat_harness_typedsocket/classes.clienttransport.ts +87 -0
- package/ts_chat_harness_typedsocket/classes.serverbinding.ts +172 -0
- package/ts_chat_harness_typedsocket/index.ts +17 -0
- package/ts_chat_harness_typedsocket/interfaces.requests.ts +40 -0
- package/ts_chat_harness_typedsocket/plugins.ts +28 -0
- package/ts_chat_harness_typedsocket/readme.md +57 -0
- package/ts_tools/readme.md +10 -0
- package/ts_tools/tool.filesystem.ts +55 -0
- package/ts_tools_node/classes.workspacereversion.ts +255 -0
- package/ts_tools_node/functions.fileimages.ts +89 -0
- package/ts_tools_node/index.ts +7 -1
- package/ts_tools_node/plugins.ts +11 -0
- package/ts_tools_node/readme.md +27 -0
- package/ts_tools_node/tool.context.ts +49 -3
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
A `FlexHarnessChatHub` serves the conversations of one FlexHarness to the `@design.estate/dees-catalog` harness chat in browser tabs, over any transport. `/chat/harness` holds its wire contract and the tab's `FlexHarnessChatClient`; `/chat/harness/typedsocket` carries both over TypedSocket.
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
pnpm add @modelprofile.com/flexharness @design.estate/dees-catalog
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { FlexHarness, createFlexToolPermissionRequester } from '@modelprofile.com/flexharness';
|
|
9
|
+
import {
|
|
10
|
+
FlexHarnessChatHub,
|
|
11
|
+
createFlexHarnessChatErrorProjector,
|
|
12
|
+
} from '@modelprofile.com/flexharness/chat/harness/server';
|
|
13
|
+
|
|
14
|
+
let hub: FlexHarnessChatHub<IEditorContext>;
|
|
15
|
+
const harness = new FlexHarness<IAppScope>({
|
|
16
|
+
scopeResolver,
|
|
17
|
+
modelResolver: {
|
|
18
|
+
resolveModel: (context) => {
|
|
19
|
+
const turn = hub.turnOf(context.scopeId, context.sessionId)!;
|
|
20
|
+
return { ...modelSetup, identity, system: buildSystem(turn.context) };
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
toolProvider: {
|
|
24
|
+
provideTools: (context) => {
|
|
25
|
+
const turn = hub.turnOf(context.scopeId, context.sessionId)!;
|
|
26
|
+
return {
|
|
27
|
+
tools: {
|
|
28
|
+
snapshot: tool({
|
|
29
|
+
description: 'Look at the page the tab shows.',
|
|
30
|
+
inputSchema: z.object({}),
|
|
31
|
+
execute: (input, options) =>
|
|
32
|
+
hub.runClientTool(turn, { toolCallId: options.toolCallId, name: 'snapshot', input }, {
|
|
33
|
+
abortSignal: options.abortSignal,
|
|
34
|
+
}),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
externalErrorProjector: createFlexHarnessChatErrorProjector(),
|
|
41
|
+
});
|
|
42
|
+
hub = new FlexHarnessChatHub<IEditorContext>({ harness, peers, readContext: parseEditorContext });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The host keeps its harness, models, system prompt, tools and scopes. It authenticates every request and calls the hub with the caller it established: the tab and the scope the tab may reach. A conversation of another scope reads as not found. A tab is named by its scope and its id together: the same tab id in two scopes names two tabs, and `IFlexHarnessChatPeers.find()` receives both. Tab ids are per-page secrets all the same; a page never shares its own.
|
|
46
|
+
|
|
47
|
+
## Conversations
|
|
48
|
+
|
|
49
|
+
- `attach(caller)` lists the scope's conversations, newest first, and forgets what the tab followed. A tab attaches after loading and after every reconnect; conversations created, renamed or deleted afterwards reach it as `flexHarnessChatSessions` pushes.
|
|
50
|
+
- `open(caller, { sessionId, subscriptionId })` follows a conversation. Its state reaches the tab as numbered `IFlexHarnessChatUpdate` pushes, starting with a `reset`: changed rows, streamed deltas, status, usage, pending permissions, undo and redo availability, and what became of steers. A tab follows one conversation at a time. An update the tab does not acknowledge makes the next change a `reset`; a tab that sees a gap in `seq` opens the conversation again.
|
|
51
|
+
- `send(caller, { sessionId?, send, context })` starts a conversation without `sessionId`. While a turn runs, the message steers it; one the turn no longer takes waits up to `admissionWaitMs` for the ending turn and starts the next one. `readContext` checks the context the message is sent with; the turn carries it. The composer's model, account and effort choices are not used: the host's model resolver decides from the context.
|
|
52
|
+
- `abort`, `respondToPermission`, `moveHistory` (FlexHarness undo and redo), `renameSession` and `deleteSession` act on a conversation of the caller's scope. An imported conversation can be read, not continued.
|
|
53
|
+
|
|
54
|
+
Refusals are `FlexHarnessChatRequestError`s with a message for the person in the tab; FlexHarness refusals keep their message. Every other error stays an internal error.
|
|
55
|
+
|
|
56
|
+
## Turns and tools in the tab
|
|
57
|
+
|
|
58
|
+
FlexHarness carries no per-prompt context, so the hub binds each turn to the tab that started it and the context it was sent with: `turnOf(scopeId, sessionId)`. A turn stops after `turnTimeoutMs`. `runClientTool(turn, request, options)` sends the call to that tab and answers with its output; it fails with `FlexHarnessChatTabUnavailableError` when the tab is gone or does not answer within `toolTimeoutMs`, and passes on the cancellation of an aborted turn.
|
|
59
|
+
|
|
60
|
+
`createFlexHarnessChatErrorProjector()` is an `externalErrorProjector` for chat conversations: the host's own projection first, then a tab that is gone, model limits, the provider's HTTP status, and a fixed message, never a provider's response text.
|
|
61
|
+
|
|
62
|
+
## Tabs and shutdown
|
|
63
|
+
|
|
64
|
+
The host implements `IFlexHarnessChatPeers.find({ tabId, scopeId })`, which reaches a tab over its current connection. Tabs that are gone and run no turn are forgotten every `sweepIntervalMs`; `forgetTab()` forgets one at once.
|
|
65
|
+
|
|
66
|
+
`stop()` refuses new requests, closes the subscriptions and clears the timers. Requests under way refuse at their next step: a message whose conversation is still being read or created starts no turn, and a turn whose prompt is being queued is cancelled as soon as the harness admits it. `stop()` cancels every turn the hub started and resolves once the requests under way were refused and those turns ended; the harness stays the host's to dispose.
|
|
67
|
+
|
|
68
|
+
## Issue Reporting and Security
|
|
69
|
+
|
|
70
|
+
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|
|
71
|
+
|
|
72
|
+
## License and Legal Information
|
|
73
|
+
|
|
74
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
|
|
75
|
+
|
|
76
|
+
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
77
|
+
|
|
78
|
+
### Trademarks
|
|
79
|
+
|
|
80
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
81
|
+
|
|
82
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
83
|
+
|
|
84
|
+
### Company Information
|
|
85
|
+
|
|
86
|
+
Task Venture Capital GmbH<br>
|
|
87
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
88
|
+
|
|
89
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
90
|
+
|
|
91
|
+
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IReq_FlexHarnessChat,
|
|
4
|
+
IReq_FlexHarnessChatSessions,
|
|
5
|
+
IReq_FlexHarnessChatTool,
|
|
6
|
+
IReq_FlexHarnessChatUpdate,
|
|
7
|
+
} from './interfaces.requests.js';
|
|
8
|
+
|
|
9
|
+
export interface IFlexHarnessChatTypedSocketTransportOptions {
|
|
10
|
+
/** The tab's TypedSocket client. */
|
|
11
|
+
typedsocket: plugins.typedsocket.TypedSocket;
|
|
12
|
+
/** The router the client was created with; the transport adds its push handlers to it. */
|
|
13
|
+
typedrouter: plugins.typedrequest.TypedRouter;
|
|
14
|
+
/** The tab: 16 random bytes in hex. Defaults to a new random id for this transport's lifetime. */
|
|
15
|
+
tabId?: string;
|
|
16
|
+
/** The opaque credential the server's `resolve` reads, read for every request. */
|
|
17
|
+
callerToken?: () => string | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 128 random bits as hex; `getRandomValues` works on plain http too, unlike `randomUUID`. */
|
|
21
|
+
const randomTabId = (): string =>
|
|
22
|
+
Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
23
|
+
|
|
24
|
+
/** A `FlexHarnessChatClient` transport over the tab's TypedSocket client. */
|
|
25
|
+
export class FlexHarnessChatTypedSocketTransport implements plugins.IFlexHarnessChatClientTransport {
|
|
26
|
+
public readonly tabId: string;
|
|
27
|
+
|
|
28
|
+
private readonly socket: plugins.typedsocket.TypedSocket;
|
|
29
|
+
private readonly router: plugins.typedrequest.TypedRouter;
|
|
30
|
+
private readonly callerToken: () => string | undefined;
|
|
31
|
+
|
|
32
|
+
constructor(optionsArg: IFlexHarnessChatTypedSocketTransportOptions) {
|
|
33
|
+
this.socket = optionsArg.typedsocket;
|
|
34
|
+
this.router = optionsArg.typedrouter;
|
|
35
|
+
this.tabId = optionsArg.tabId ?? randomTabId();
|
|
36
|
+
this.callerToken = optionsArg.callerToken ?? (() => undefined);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async request<TMethod extends plugins.TFlexHarnessChatMethod>(
|
|
40
|
+
methodArg: TMethod,
|
|
41
|
+
requestArg: plugins.TFlexHarnessChatRequest<TMethod>,
|
|
42
|
+
): Promise<plugins.TFlexHarnessChatResponse<TMethod>> {
|
|
43
|
+
const token = this.callerToken();
|
|
44
|
+
return this.socket.createTypedRequest<IReq_FlexHarnessChat<TMethod>>(methodArg).fire({
|
|
45
|
+
...requestArg,
|
|
46
|
+
caller: { tabId: this.tabId, ...(token === undefined ? {} : { token }) },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public listen(handlersArg: plugins.IFlexHarnessChatPushHandlers): () => void {
|
|
51
|
+
const pushes = new plugins.typedrequest.TypedRouter();
|
|
52
|
+
pushes.addTypedHandler(
|
|
53
|
+
new plugins.typedrequest.TypedHandler<IReq_FlexHarnessChatUpdate>('flexHarnessChatUpdate', async (pushArg) => {
|
|
54
|
+
handlersArg.update(pushArg);
|
|
55
|
+
return {};
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
pushes.addTypedHandler(
|
|
59
|
+
new plugins.typedrequest.TypedHandler<IReq_FlexHarnessChatSessions>('flexHarnessChatSessions', async (pushArg) => {
|
|
60
|
+
handlersArg.sessions(pushArg);
|
|
61
|
+
return {};
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
pushes.addTypedHandler(
|
|
65
|
+
new plugins.typedrequest.TypedHandler<IReq_FlexHarnessChatTool>('flexHarnessChatTool', async (requestArg, toolsArg) => ({
|
|
66
|
+
// the server cancels the request when the turn stops or the call times out; the handler then
|
|
67
|
+
// settles at once, since TypedSocket fails a connection whose handlers outlive their requests
|
|
68
|
+
output: await handlersArg.tool(requestArg, toolsArg?.abortSignal),
|
|
69
|
+
})),
|
|
70
|
+
);
|
|
71
|
+
return this.router.addTypedRouter(pushes);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public onReconnect(callbackArg: () => void): () => void {
|
|
75
|
+
let wasConnected = true;
|
|
76
|
+
const subscription = this.socket.statusSubject.subscribe((statusArg) => {
|
|
77
|
+
if (statusArg !== 'connected') {
|
|
78
|
+
wasConnected = false;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (wasConnected) return;
|
|
82
|
+
wasConnected = true;
|
|
83
|
+
callbackArg();
|
|
84
|
+
});
|
|
85
|
+
return () => subscription.unsubscribe();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IFlexHarnessChatCallerPayload,
|
|
4
|
+
IReq_FlexHarnessChat,
|
|
5
|
+
IReq_FlexHarnessChatSessions,
|
|
6
|
+
IReq_FlexHarnessChatTool,
|
|
7
|
+
IReq_FlexHarnessChatUpdate,
|
|
8
|
+
} from './interfaces.requests.js';
|
|
9
|
+
|
|
10
|
+
/** The hub requests the binding routes; any `FlexHarnessChatHub` serves them. */
|
|
11
|
+
export type TFlexHarnessChatRequestHandler = Pick<
|
|
12
|
+
plugins.FlexHarnessChatHub<unknown>,
|
|
13
|
+
'attach' | 'open' | 'close' | 'send' | 'abort' | 'respondToPermission' | 'moveHistory' | 'renameSession' | 'deleteSession'
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
/** The hub and scope a request reaches, as the host established them. */
|
|
17
|
+
export interface IFlexHarnessChatResolvedCaller {
|
|
18
|
+
hub: TFlexHarnessChatRequestHandler;
|
|
19
|
+
scopeId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IFlexHarnessChatTypedSocketBindingOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Authenticates a request: from the caller payload and the request's TypedSocket context, the hub
|
|
25
|
+
* and the scope it reaches. Throw a `FlexHarnessChatRequestError` to refuse with a message.
|
|
26
|
+
*/
|
|
27
|
+
resolve(
|
|
28
|
+
caller: IFlexHarnessChatCallerPayload,
|
|
29
|
+
context: plugins.typedsocket.ITypedSocketRequestContext | undefined,
|
|
30
|
+
): Promise<IFlexHarnessChatResolvedCaller>;
|
|
31
|
+
/** The server-owned connection tag that names a tab. Default: `flexHarnessChatTab`. */
|
|
32
|
+
tag?: string;
|
|
33
|
+
/** How long an update or a session-list push may wait for the tab. Default: 30 seconds. */
|
|
34
|
+
pushTimeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TAB_ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
38
|
+
const BAD_REQUEST_MESSAGE = 'The request is malformed. Reload the page and try again.';
|
|
39
|
+
const NOT_READY_MESSAGE = 'The server is not accepting connections yet.';
|
|
40
|
+
|
|
41
|
+
/** The tag payload that names a tab: its scope and its id, in a fixed key order. */
|
|
42
|
+
const tagPayload = (tabArg: plugins.IFlexHarnessChatCaller): { scopeId: string; tabId: string } => ({
|
|
43
|
+
scopeId: tabArg.scopeId,
|
|
44
|
+
tabId: tabArg.tabId,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
class TypedSocketChatPeer implements plugins.IFlexHarnessChatPeer {
|
|
48
|
+
constructor(
|
|
49
|
+
private readonly socket: plugins.typedsocket.TypedSocket,
|
|
50
|
+
private readonly connection: plugins.typedsocket.ISmartServeConnectionWrapper,
|
|
51
|
+
private readonly pushTimeoutMs: number,
|
|
52
|
+
) {}
|
|
53
|
+
|
|
54
|
+
public async update(pushArg: plugins.IFlexHarnessChatUpdate): Promise<void> {
|
|
55
|
+
await this.socket
|
|
56
|
+
.createTypedRequest<IReq_FlexHarnessChatUpdate>('flexHarnessChatUpdate', this.connection, { timeoutMs: this.pushTimeoutMs })
|
|
57
|
+
.fire(pushArg);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async sessions(pushArg: plugins.IFlexHarnessChatSessionsPush): Promise<void> {
|
|
61
|
+
await this.socket
|
|
62
|
+
.createTypedRequest<IReq_FlexHarnessChatSessions>('flexHarnessChatSessions', this.connection, { timeoutMs: this.pushTimeoutMs })
|
|
63
|
+
.fire(pushArg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async runTool(
|
|
67
|
+
requestArg: plugins.IFlexHarnessChatToolRequest,
|
|
68
|
+
optionsArg: { timeoutMs: number; abortSignal?: AbortSignal },
|
|
69
|
+
): Promise<plugins.TJsonValue> {
|
|
70
|
+
const answer = await this.socket
|
|
71
|
+
.createTypedRequest<IReq_FlexHarnessChatTool>('flexHarnessChatTool', this.connection, optionsArg)
|
|
72
|
+
.fire(requestArg);
|
|
73
|
+
return answer.output;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Serves a `FlexHarnessChatHub` over TypedSocket. Add `typedrouter` to the server's router, hand
|
|
79
|
+
* the server's TypedSocket to `setTypedSocket()` once it runs, and give the hub `peers`. A tab's
|
|
80
|
+
* connection carries the server-owned tag with its tab id; every request claims it again.
|
|
81
|
+
*/
|
|
82
|
+
export class FlexHarnessChatTypedSocketBinding implements plugins.IFlexHarnessChatPeers {
|
|
83
|
+
public readonly typedrouter = new plugins.typedrequest.TypedRouter();
|
|
84
|
+
|
|
85
|
+
private readonly resolveCaller: IFlexHarnessChatTypedSocketBindingOptions['resolve'];
|
|
86
|
+
private readonly tag: string;
|
|
87
|
+
private readonly pushTimeoutMs: number;
|
|
88
|
+
private socket: plugins.typedsocket.TypedSocket | null = null;
|
|
89
|
+
|
|
90
|
+
constructor(optionsArg: IFlexHarnessChatTypedSocketBindingOptions) {
|
|
91
|
+
this.resolveCaller = optionsArg.resolve;
|
|
92
|
+
this.tag = optionsArg.tag ?? 'flexHarnessChatTab';
|
|
93
|
+
this.pushTimeoutMs = optionsArg.pushTimeoutMs ?? 30 * 1000;
|
|
94
|
+
this.route<'flexHarnessChatAttach'>('flexHarnessChatAttach', (request, tools) =>
|
|
95
|
+
this.serve(request.caller, tools, (hub, caller) => hub.attach(caller)));
|
|
96
|
+
this.route<'flexHarnessChatOpen'>('flexHarnessChatOpen', (request, tools) =>
|
|
97
|
+
this.serve(request.caller, tools, (hub, caller) => hub.open(caller, request)));
|
|
98
|
+
this.route<'flexHarnessChatClose'>('flexHarnessChatClose', (request, tools) =>
|
|
99
|
+
this.serve(request.caller, tools, (hub, caller) => hub.close(caller, request)));
|
|
100
|
+
this.route<'flexHarnessChatSend'>('flexHarnessChatSend', (request, tools) =>
|
|
101
|
+
this.serve(request.caller, tools, (hub, caller) => hub.send(caller, request)));
|
|
102
|
+
this.route<'flexHarnessChatAbort'>('flexHarnessChatAbort', (request, tools) =>
|
|
103
|
+
this.serve(request.caller, tools, (hub, caller) => hub.abort(caller, request)));
|
|
104
|
+
this.route<'flexHarnessChatPermission'>('flexHarnessChatPermission', (request, tools) =>
|
|
105
|
+
this.serve(request.caller, tools, (hub, caller) => hub.respondToPermission(caller, request)));
|
|
106
|
+
this.route<'flexHarnessChatHistory'>('flexHarnessChatHistory', (request, tools) =>
|
|
107
|
+
this.serve(request.caller, tools, (hub, caller) => hub.moveHistory(caller, request)));
|
|
108
|
+
this.route<'flexHarnessChatSessionUpdate'>('flexHarnessChatSessionUpdate', (request, tools) =>
|
|
109
|
+
this.serve(request.caller, tools, (hub, caller) => hub.renameSession(caller, request)));
|
|
110
|
+
this.route<'flexHarnessChatSessionDelete'>('flexHarnessChatSessionDelete', (request, tools) =>
|
|
111
|
+
this.serve(request.caller, tools, (hub, caller) => hub.deleteSession(caller, request)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The server's TypedSocket; it exists once the server runs. */
|
|
115
|
+
public setTypedSocket(socketArg: plugins.typedsocket.TypedSocket): void {
|
|
116
|
+
this.socket = socketArg;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The tab's current connection in its scope, as the hub reaches it. */
|
|
120
|
+
public async find(tabArg: plugins.IFlexHarnessChatCaller): Promise<plugins.IFlexHarnessChatPeer | null> {
|
|
121
|
+
if (!this.socket) return null;
|
|
122
|
+
const connection = await this.socket.findTargetConnectionByTag(this.tag, tagPayload(tabArg));
|
|
123
|
+
return connection ? new TypedSocketChatPeer(this.socket, connection, this.pushTimeoutMs) : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private route<TMethod extends plugins.TFlexHarnessChatMethod>(
|
|
127
|
+
methodArg: TMethod,
|
|
128
|
+
handlerArg: plugins.typedrequest.THandlerFunction<IReq_FlexHarnessChat<TMethod>>,
|
|
129
|
+
): void {
|
|
130
|
+
this.typedrouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_FlexHarnessChat<TMethod>>(methodArg, handlerArg));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Authenticates a request, claims its connection for the tab and runs it on the resolved hub. */
|
|
134
|
+
private async serve<T>(
|
|
135
|
+
payloadArg: IFlexHarnessChatCallerPayload | undefined,
|
|
136
|
+
contextArg: plugins.typedsocket.ITypedSocketRequestContext | undefined,
|
|
137
|
+
runArg: (hubArg: TFlexHarnessChatRequestHandler, callerArg: plugins.IFlexHarnessChatCaller) => Promise<T>,
|
|
138
|
+
): Promise<T> {
|
|
139
|
+
try {
|
|
140
|
+
if (!payloadArg || typeof payloadArg.tabId !== 'string' || !TAB_ID_PATTERN.test(payloadArg.tabId)) {
|
|
141
|
+
throw new plugins.FlexHarnessChatRequestError(BAD_REQUEST_MESSAGE);
|
|
142
|
+
}
|
|
143
|
+
const resolved = await this.resolveCaller(payloadArg, contextArg);
|
|
144
|
+
const caller = { tabId: payloadArg.tabId, scopeId: resolved.scopeId };
|
|
145
|
+
await this.claim(contextArg, caller);
|
|
146
|
+
return await runArg(resolved.hub, caller);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof plugins.FlexHarnessChatRequestError) {
|
|
149
|
+
throw new plugins.typedrequest.TypedResponseError(error.message);
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Marks the connection the request came in on as the tab's, and only that one. The tag names the
|
|
157
|
+
* tab in its resolved scope, so a caller of another scope that sends the same tab id names a tab
|
|
158
|
+
* of its own and never reaches this one's connection.
|
|
159
|
+
*/
|
|
160
|
+
private async claim(
|
|
161
|
+
contextArg: plugins.typedsocket.ITypedSocketRequestContext | undefined,
|
|
162
|
+
tabArg: plugins.IFlexHarnessChatCaller,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
if (!this.socket) throw new plugins.FlexHarnessChatRequestError(NOT_READY_MESSAGE);
|
|
165
|
+
const connection = this.socket.getServerConnectionForRequest(contextArg);
|
|
166
|
+
const payload = tagPayload(tabArg);
|
|
167
|
+
for (const other of await this.socket.findAllTargetConnectionsByTag(this.tag, payload)) {
|
|
168
|
+
if (other.peer !== connection.peer) this.socket.removeServerTag(other, this.tag);
|
|
169
|
+
}
|
|
170
|
+
this.socket.setServerTag(connection, this.tag, payload);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
FlexHarnessChatTypedSocketBinding,
|
|
3
|
+
type IFlexHarnessChatResolvedCaller,
|
|
4
|
+
type IFlexHarnessChatTypedSocketBindingOptions,
|
|
5
|
+
type TFlexHarnessChatRequestHandler,
|
|
6
|
+
} from './classes.serverbinding.js';
|
|
7
|
+
export {
|
|
8
|
+
FlexHarnessChatTypedSocketTransport,
|
|
9
|
+
type IFlexHarnessChatTypedSocketTransportOptions,
|
|
10
|
+
} from './classes.clienttransport.js';
|
|
11
|
+
export type {
|
|
12
|
+
IFlexHarnessChatCallerPayload,
|
|
13
|
+
IReq_FlexHarnessChat,
|
|
14
|
+
IReq_FlexHarnessChatSessions,
|
|
15
|
+
IReq_FlexHarnessChatTool,
|
|
16
|
+
IReq_FlexHarnessChatUpdate,
|
|
17
|
+
} from './interfaces.requests.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
/** Who sends a request: the tab, and an opaque credential the host's `resolve` reads. */
|
|
4
|
+
export interface IFlexHarnessChatCallerPayload {
|
|
5
|
+
/**
|
|
6
|
+
* The tab: 16 random bytes in hex, kept for the tab's lifetime and never shared. Every request
|
|
7
|
+
* claims the connection it arrives on for the tab, so pushes and tool calls follow a reconnect.
|
|
8
|
+
*/
|
|
9
|
+
tabId: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A tab's request to the hub over TypedSocket. */
|
|
14
|
+
export interface IReq_FlexHarnessChat<TMethod extends plugins.TFlexHarnessChatMethod>
|
|
15
|
+
extends plugins.typedrequestInterfaces.ITypedRequest {
|
|
16
|
+
method: TMethod;
|
|
17
|
+
request: plugins.TFlexHarnessChatRequest<TMethod> & { caller: IFlexHarnessChatCallerPayload };
|
|
18
|
+
response: plugins.TFlexHarnessChatResponse<TMethod>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Push: what changed in the conversation a tab follows. */
|
|
22
|
+
export interface IReq_FlexHarnessChatUpdate extends plugins.typedrequestInterfaces.ITypedRequest {
|
|
23
|
+
method: 'flexHarnessChatUpdate';
|
|
24
|
+
request: plugins.IFlexHarnessChatUpdate;
|
|
25
|
+
response: Record<string, never>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Push: changes of the tab's conversation list. */
|
|
29
|
+
export interface IReq_FlexHarnessChatSessions extends plugins.typedrequestInterfaces.ITypedRequest {
|
|
30
|
+
method: 'flexHarnessChatSessions';
|
|
31
|
+
request: plugins.IFlexHarnessChatSessionsPush;
|
|
32
|
+
response: Record<string, never>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Push: run a tool of a turn in the tab; the answer is the tool's output. */
|
|
36
|
+
export interface IReq_FlexHarnessChatTool extends plugins.typedrequestInterfaces.ITypedRequest {
|
|
37
|
+
method: 'flexHarnessChatTool';
|
|
38
|
+
request: plugins.IFlexHarnessChatToolRequest;
|
|
39
|
+
response: { output: plugins.TJsonValue };
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @api.global scope
|
|
2
|
+
import * as typedrequest from '@api.global/typedrequest';
|
|
3
|
+
import * as typedsocket from '@api.global/typedsocket';
|
|
4
|
+
|
|
5
|
+
export { typedrequest, typedsocket };
|
|
6
|
+
export type * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
|
7
|
+
|
|
8
|
+
// @modelprofile.com scope
|
|
9
|
+
import { FlexHarnessChatRequestError } from '@modelprofile.com/flexharness/chat/harness';
|
|
10
|
+
|
|
11
|
+
export { FlexHarnessChatRequestError };
|
|
12
|
+
export type { TJsonValue } from '@modelprofile.com/flexharness';
|
|
13
|
+
export type {
|
|
14
|
+
IFlexHarnessChatClientTransport,
|
|
15
|
+
IFlexHarnessChatPushHandlers,
|
|
16
|
+
IFlexHarnessChatSessionsPush,
|
|
17
|
+
IFlexHarnessChatToolRequest,
|
|
18
|
+
IFlexHarnessChatUpdate,
|
|
19
|
+
TFlexHarnessChatMethod,
|
|
20
|
+
TFlexHarnessChatRequest,
|
|
21
|
+
TFlexHarnessChatResponse,
|
|
22
|
+
} from '@modelprofile.com/flexharness/chat/harness';
|
|
23
|
+
export type {
|
|
24
|
+
FlexHarnessChatHub,
|
|
25
|
+
IFlexHarnessChatCaller,
|
|
26
|
+
IFlexHarnessChatPeer,
|
|
27
|
+
IFlexHarnessChatPeers,
|
|
28
|
+
} from '@modelprofile.com/flexharness/chat/harness/server';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
`/chat/harness` and its hub over TypedSocket: the server routes every request of a tab to a `FlexHarnessChatHub`, and the tab's `FlexHarnessChatClient` sends and receives through its TypedSocket client.
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
pnpm add @modelprofile.com/flexharness @api.global/typedsocket @api.global/typedrequest @api.global/typedrequest-interfaces
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
The TypedSocket packages are optional peers: `@api.global/typedsocket` `^9.0.2` with `@api.global/typedrequest` `^9.1.0`, the major TypedSocket uses.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// server
|
|
11
|
+
import { FlexHarnessChatTypedSocketBinding } from '@modelprofile.com/flexharness/chat/harness/typedsocket';
|
|
12
|
+
|
|
13
|
+
const binding = new FlexHarnessChatTypedSocketBinding({
|
|
14
|
+
// authenticate the request; the scope never comes from the browser
|
|
15
|
+
resolve: async (caller, context) => ({ hub, scopeId: await scopeOf(caller.token) }),
|
|
16
|
+
});
|
|
17
|
+
hub = new FlexHarnessChatHub({ harness, peers: binding, readContext });
|
|
18
|
+
server.typedrouter.addTypedRouter(binding.typedrouter);
|
|
19
|
+
await server.start();
|
|
20
|
+
binding.setTypedSocket(server.typedsocket);
|
|
21
|
+
|
|
22
|
+
// tab
|
|
23
|
+
import { FlexHarnessChatClient } from '@modelprofile.com/flexharness/chat/harness';
|
|
24
|
+
import { FlexHarnessChatTypedSocketTransport } from '@modelprofile.com/flexharness/chat/harness/typedsocket';
|
|
25
|
+
|
|
26
|
+
const transport = new FlexHarnessChatTypedSocketTransport({ typedsocket, typedrouter, callerToken: () => sessionKey });
|
|
27
|
+
const client = new FlexHarnessChatClient({ transport });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every request carries `caller: { tabId, token? }`. `resolve` reads it and the request's TypedSocket context and returns the hub and scope it reaches; a `FlexHarnessChatRequestError` it throws reaches the tab as a `TypedResponseError` with its message. The tab id is 16 random bytes in hex, kept for the tab's lifetime and never shared: every request claims the connection it arrives on for the tab with the server-owned tag (`flexHarnessChatTab` by default), so updates, list pushes and tool calls follow a reconnect. The tag's payload is the resolved scope and the tab id: a caller of another scope that sends the same tab id names a tab of its own and never takes over this one's connection, its updates or its tool calls. Pushes wait `pushTimeoutMs` (30 seconds) for the tab.
|
|
31
|
+
|
|
32
|
+
The transport adds its push handlers (`flexHarnessChatUpdate`, `flexHarnessChatSessions`, `flexHarnessChatTool`) to the tab's router and removes them when the client is disposed. A tool call the server cancels settles at once in the tab. A restored connection makes the client attach again.
|
|
33
|
+
|
|
34
|
+
## Issue Reporting and Security
|
|
35
|
+
|
|
36
|
+
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|
|
37
|
+
|
|
38
|
+
## License and Legal Information
|
|
39
|
+
|
|
40
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
|
|
41
|
+
|
|
42
|
+
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
43
|
+
|
|
44
|
+
### Trademarks
|
|
45
|
+
|
|
46
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
47
|
+
|
|
48
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
49
|
+
|
|
50
|
+
### Company Information
|
|
51
|
+
|
|
52
|
+
Task Venture Capital GmbH<br>
|
|
53
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
54
|
+
|
|
55
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
56
|
+
|
|
57
|
+
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
package/ts_tools/readme.md
CHANGED
|
@@ -11,6 +11,16 @@ pnpm add @modelprofile.com/flexharness
|
|
|
11
11
|
See the [toolbox usage and migration guide](https://code.foss.global/modelprofile.com/flexharness#readme)
|
|
12
12
|
for composition examples and the SmartAI/SmartAgent migration map.
|
|
13
13
|
|
|
14
|
+
## Filesystem tools
|
|
15
|
+
|
|
16
|
+
`createFilesystemTools(context, options)` exposes `read_file`, `write_file`, `list_directory` and, unless `includeDelete: false`, `delete_file` over the context's `fs`. `write_file` and `delete_file` ask `context.requestPermission` (`write`, `delete`) before they change anything.
|
|
17
|
+
|
|
18
|
+
`includeEdit: true` adds `edit_file { path, find, replace, replaceAll? }`. It replaces one exact occurrence of `find`, or every occurrence with `replaceAll`, and refuses a `find` that does not occur or, without `replaceAll`, occurs more than once. The replacement text is literal. It asks for `write` permission before it reads the file, so an edit made while the request waits is kept. It is off by default, so the tools an existing agent sees do not change.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
const tools = createFilesystemTools(context, { includeEdit: true });
|
|
22
|
+
```
|
|
23
|
+
|
|
14
24
|
## Issue Reporting and Security
|
|
15
25
|
|
|
16
26
|
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
export interface ICreateFilesystemToolsOptions {
|
|
8
8
|
/** Include delete_file. Default: true. */
|
|
9
9
|
includeDelete?: boolean;
|
|
10
|
+
/** Include edit_file, which replaces an exact snippet of a file. Default: false. */
|
|
11
|
+
includeEdit?: boolean;
|
|
10
12
|
/** Maximum output lines before truncating. */
|
|
11
13
|
maxLines?: number;
|
|
12
14
|
/** Maximum output bytes before truncating. */
|
|
@@ -110,6 +112,59 @@ export function createFilesystemTools(context: IToolExecutionContext, options: I
|
|
|
110
112
|
}),
|
|
111
113
|
};
|
|
112
114
|
|
|
115
|
+
if (options.includeEdit === true) {
|
|
116
|
+
tools.edit_file = plugins.tool({
|
|
117
|
+
description:
|
|
118
|
+
'Replace an exact snippet of a UTF-8 file in the active workspace. `find` must occur exactly once unless `replaceAll` is set; include enough surrounding text to make it unique. Requires host permission when configured.',
|
|
119
|
+
inputSchema: plugins.z.object({
|
|
120
|
+
path: plugins.z.string().describe('File path'),
|
|
121
|
+
find: plugins.z.string().min(1).describe('Exact text to replace, copied from the current file'),
|
|
122
|
+
replace: plugins.z.string().describe('The text to put in its place'),
|
|
123
|
+
replaceAll: plugins.z
|
|
124
|
+
.boolean()
|
|
125
|
+
.optional()
|
|
126
|
+
.describe('Replace every occurrence instead of exactly one. Default: false'),
|
|
127
|
+
}),
|
|
128
|
+
execute: async (
|
|
129
|
+
{
|
|
130
|
+
path: filePath,
|
|
131
|
+
find,
|
|
132
|
+
replace,
|
|
133
|
+
replaceAll,
|
|
134
|
+
}: { path: string; find: string; replace: string; replaceAll?: boolean },
|
|
135
|
+
invocationOptions?: plugins.ToolExecutionOptions<unknown>,
|
|
136
|
+
) => {
|
|
137
|
+
if (!context.fs) {
|
|
138
|
+
throw new Error('Filesystem tools are not available in this execution context.');
|
|
139
|
+
}
|
|
140
|
+
const abortSignal = invocationOptions?.abortSignal ?? context.abortSignal;
|
|
141
|
+
const invocation = { toolCallId: invocationOptions?.toolCallId, abortSignal };
|
|
142
|
+
// asked before the file is read: an edit made while the request waits is kept, not overwritten
|
|
143
|
+
await context.requestPermission?.({
|
|
144
|
+
type: 'write',
|
|
145
|
+
title: 'Edit file',
|
|
146
|
+
metadata: { path: filePath },
|
|
147
|
+
toolCallId: invocationOptions?.toolCallId,
|
|
148
|
+
abortSignal,
|
|
149
|
+
});
|
|
150
|
+
const content = await context.fs.readFile(filePath, undefined, invocation);
|
|
151
|
+
const occurrences = content.split(find).length - 1;
|
|
152
|
+
if (occurrences === 0) {
|
|
153
|
+
throw new Error(`The text to replace does not occur in ${filePath}.`);
|
|
154
|
+
}
|
|
155
|
+
if (occurrences > 1 && replaceAll !== true) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`The text to replace occurs ${occurrences} times in ${filePath}; include more surrounding text or set replaceAll.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const updated = replaceAll === true ? content.split(find).join(replace) : content.replace(find, () => replace);
|
|
161
|
+
await context.fs.writeFile(filePath, updated, invocation);
|
|
162
|
+
const replaced = replaceAll === true ? occurrences : 1;
|
|
163
|
+
return truncate(`Replaced ${replaced} occurrence${replaced === 1 ? '' : 's'} in ${filePath}`);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
113
168
|
if (options.includeDelete !== false) {
|
|
114
169
|
tools.delete_file = plugins.tool({
|
|
115
170
|
description: 'Delete a file or empty directory in the active workspace. Requires host permission when configured.',
|