@portel/photon 1.23.1 → 1.25.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/README.md +66 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +262 -18
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/beam.bundle.js +58287 -56177
- package/dist/beam.bundle.js.map +4 -4
- package/dist/capability-negotiator.d.ts +9 -0
- package/dist/capability-negotiator.d.ts.map +1 -1
- package/dist/capability-negotiator.js +14 -0
- package/dist/capability-negotiator.js.map +1 -1
- package/dist/cli/commands/claim.d.ts +17 -0
- package/dist/cli/commands/claim.d.ts.map +1 -0
- package/dist/cli/commands/claim.js +124 -0
- package/dist/cli/commands/claim.js.map +1 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +2 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/claims.d.ts +108 -0
- package/dist/daemon/claims.d.ts.map +1 -0
- package/dist/daemon/claims.js +245 -0
- package/dist/daemon/claims.js.map +1 -0
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +15 -29
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/cron.d.ts +36 -0
- package/dist/daemon/cron.d.ts.map +1 -0
- package/dist/daemon/cron.js +216 -0
- package/dist/daemon/cron.js.map +1 -0
- package/dist/daemon/schedule-loader.d.ts +76 -0
- package/dist/daemon/schedule-loader.d.ts.map +1 -0
- package/dist/daemon/schedule-loader.js +124 -0
- package/dist/daemon/schedule-loader.js.map +1 -0
- package/dist/daemon/server.js +76 -226
- package/dist/daemon/server.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +154 -4
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/loader.d.ts +22 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +246 -30
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +32 -2
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +50 -1
- package/dist/server.js.map +1 -1
- package/dist/shared/memory-sqlite.d.ts +37 -0
- package/dist/shared/memory-sqlite.d.ts.map +1 -0
- package/dist/shared/memory-sqlite.js +143 -0
- package/dist/shared/memory-sqlite.js.map +1 -0
- package/dist/telemetry/context.d.ts +9 -0
- package/dist/telemetry/context.d.ts.map +1 -1
- package/dist/telemetry/context.js.map +1 -1
- package/package.json +4 -4
- package/templates/cloudflare/worker.ts.template +53 -74
- package/templates/cloudflare/wrangler.toml.template +6 -1
package/dist/loader.js
CHANGED
|
@@ -12,7 +12,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
|
|
|
12
12
|
import * as crypto from 'crypto';
|
|
13
13
|
import { startToolSpan } from './telemetry/otel.js';
|
|
14
14
|
import { recordToolCall, recordCircuitStateChange, recordRateLimitRejection, recordBulkheadRejection, } from './telemetry/metrics.js';
|
|
15
|
-
import { runWithRequestContext } from './telemetry/context.js';
|
|
15
|
+
import { runWithRequestContext, getRequestContext } from './telemetry/context.js';
|
|
16
16
|
import { spawn, execSync } from 'child_process';
|
|
17
17
|
import { SchemaExtractor, DependencyManager,
|
|
18
18
|
// Generator utilities (ask/emit pattern from 1.2.0)
|
|
@@ -164,6 +164,68 @@ function injectEmitHelpers(instance) {
|
|
|
164
164
|
function detectEmitHelperUsage(source) {
|
|
165
165
|
return /this\.(toast|log|status|progress|thinking)\s*\(/.test(source);
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Always-inject `this.sample`, `this.confirm`, and `this.elicit` on plain
|
|
169
|
+
* classes. Each pulls its provider out of the ALS execution context
|
|
170
|
+
* (set by the loader when it invokes a tool). User-defined methods on
|
|
171
|
+
* the class win — these wrappers only fill in when absent.
|
|
172
|
+
*
|
|
173
|
+
* Mirrors the Photon base class implementations in photon-core/base.ts,
|
|
174
|
+
* so a plain class and a class that extends Photon behave identically.
|
|
175
|
+
*/
|
|
176
|
+
function injectSamplingAndElicitation(instance) {
|
|
177
|
+
if (!instance.confirm) {
|
|
178
|
+
instance.confirm = async (question) => {
|
|
179
|
+
const store = executionContext.getStore();
|
|
180
|
+
if (!store?.inputProvider) {
|
|
181
|
+
throw new Error('this.confirm() requires a connected MCP client that supports ' +
|
|
182
|
+
'elicitation. None is attached to the current invocation.');
|
|
183
|
+
}
|
|
184
|
+
const result = await store.inputProvider({
|
|
185
|
+
ask: 'confirm',
|
|
186
|
+
question,
|
|
187
|
+
message: question,
|
|
188
|
+
});
|
|
189
|
+
return Boolean(result);
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (!instance.elicit) {
|
|
193
|
+
instance.elicit = async (params) => {
|
|
194
|
+
const store = executionContext.getStore();
|
|
195
|
+
if (!store?.inputProvider) {
|
|
196
|
+
throw new Error('this.elicit() requires a connected MCP client that supports ' +
|
|
197
|
+
'elicitation. None is attached to the current invocation.');
|
|
198
|
+
}
|
|
199
|
+
return await store.inputProvider(params);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (!instance.sample) {
|
|
203
|
+
instance.sample = async (params) => {
|
|
204
|
+
const store = executionContext.getStore();
|
|
205
|
+
if (!store?.samplingProvider) {
|
|
206
|
+
throw new Error('this.sample() requires the connected MCP client to declare the ' +
|
|
207
|
+
'`sampling` capability. None is available in this invocation.');
|
|
208
|
+
}
|
|
209
|
+
if (!params.prompt && !params.messages?.length) {
|
|
210
|
+
throw new Error('this.sample() requires either `prompt` or `messages`.');
|
|
211
|
+
}
|
|
212
|
+
const messages = params.messages ?? [
|
|
213
|
+
{ role: 'user', content: { type: 'text', text: params.prompt } },
|
|
214
|
+
];
|
|
215
|
+
const result = await store.samplingProvider({
|
|
216
|
+
messages,
|
|
217
|
+
systemPrompt: params.systemPrompt,
|
|
218
|
+
maxTokens: params.maxTokens ?? 1024,
|
|
219
|
+
temperature: params.temperature,
|
|
220
|
+
modelPreferences: params.modelPreferences,
|
|
221
|
+
stopSequences: params.stopSequences,
|
|
222
|
+
includeContext: params.includeContext,
|
|
223
|
+
});
|
|
224
|
+
const first = Array.isArray(result.content) ? result.content[0] : result.content;
|
|
225
|
+
return first?.type === 'text' ? first.text : '';
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
167
229
|
/**
|
|
168
230
|
* Render a formatted value in the CLI using @portel/cli's formatOutput.
|
|
169
231
|
* Uses clear-and-replace semantics — each call overwrites the previous render.
|
|
@@ -230,6 +292,14 @@ export class PhotonLoader {
|
|
|
230
292
|
marketplaceManager;
|
|
231
293
|
marketplaceManagerPromise;
|
|
232
294
|
logger;
|
|
295
|
+
/**
|
|
296
|
+
* Per-instance call queue. Two tool invocations targeting the same photon
|
|
297
|
+
* instance are serialized: the second starts only after the first returns.
|
|
298
|
+
* Prevents lost-update and read-after-write races between methods that share
|
|
299
|
+
* state via `this.memory` or instance fields. Keyed on the instance object
|
|
300
|
+
* via WeakMap so hot-reload drops the queue automatically.
|
|
301
|
+
*/
|
|
302
|
+
_instanceCallTails = new WeakMap();
|
|
233
303
|
// ════════════════════════════════════════════════════════════════════════════
|
|
234
304
|
// MIDDLEWARE STATE
|
|
235
305
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -695,6 +765,31 @@ export class PhotonLoader {
|
|
|
695
765
|
instance.instanceName = options?.instanceName ?? '';
|
|
696
766
|
// Inject file path for storage()/assets() resolution
|
|
697
767
|
instance._photonFilePath = absolutePath;
|
|
768
|
+
// Inject schedule-unschedule hook so `this.schedule.cancel()` evicts
|
|
769
|
+
// the in-memory cron registration at the same time as unlinking
|
|
770
|
+
// the file. Prevents ghost schedule registrations — the scenario
|
|
771
|
+
// where a cancel + re-enable under the same name leaves the old
|
|
772
|
+
// cron firing in parallel with the new one until the next daemon
|
|
773
|
+
// restart. The hook is best-effort: if the daemon is unreachable
|
|
774
|
+
// the fire-time phantom-prune (daemon checks sourceFile before
|
|
775
|
+
// running) catches the ghost on its next tick.
|
|
776
|
+
const photonNameForSchedule = name;
|
|
777
|
+
const scheduleLogger = this.logger;
|
|
778
|
+
instance._scheduleUnscheduleHook = async (jobId) => {
|
|
779
|
+
try {
|
|
780
|
+
const { unscheduleJob } = await import('./daemon/client.js');
|
|
781
|
+
return await unscheduleJob(photonNameForSchedule, jobId);
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
// Daemon unreachable or IPC failure — log loudly so operators
|
|
785
|
+
// notice. Phantom-prune (daemon checks sourceFile before
|
|
786
|
+
// running) still evicts the ghost on its next fire, but for
|
|
787
|
+
// low-frequency schedules that may be hours away. Returning
|
|
788
|
+
// false lets `schedule.cancel` surface the outcome too.
|
|
789
|
+
scheduleLogger.warn(`[schedule] failed to evict in-memory cron registration for ${photonNameForSchedule}:${jobId}`, { error: err instanceof Error ? err.message : String(err) });
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
698
793
|
// Inject this.shell — execSync wrapper with cwd defaulted to the
|
|
699
794
|
// photon's own folder. Shelling out from a photon method should run
|
|
700
795
|
// from where the photon lives, not the daemon's cwd, so that
|
|
@@ -778,27 +873,33 @@ export class PhotonLoader {
|
|
|
778
873
|
if (typeof instance._callHandler === 'undefined' || instance._callHandler === undefined) {
|
|
779
874
|
const callBaseDir = this.baseDir;
|
|
780
875
|
instance._callHandler = async (photonName, method, params, targetInstance) => {
|
|
781
|
-
// Propagate trace context to nested photon-to-photon
|
|
782
|
-
// downstream span chains as a child of the current span
|
|
783
|
-
//
|
|
784
|
-
//
|
|
876
|
+
// Propagate trace context and caller cwd to nested photon-to-photon
|
|
877
|
+
// calls so the downstream span chains as a child of the current span
|
|
878
|
+
// and the callee can resolve relative paths against the originating
|
|
879
|
+
// CLI directory. Uses in-band `_meta` which executeTool peeks at
|
|
880
|
+
// before schema validation: works through worker threads transparently.
|
|
785
881
|
const ctxMod = await import('./telemetry/context.js');
|
|
786
882
|
const ctx = ctxMod.getRequestContext();
|
|
787
883
|
let forwardedParams = params;
|
|
884
|
+
const metaAdditions = {};
|
|
788
885
|
if (ctx) {
|
|
789
886
|
const traceparent = ctx.parentTraceparent ||
|
|
790
887
|
(ctx.traceId
|
|
791
888
|
? `00-${ctx.traceId}-${crypto.randomBytes(8).toString('hex')}-01`
|
|
792
889
|
: undefined);
|
|
793
|
-
if (traceparent)
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
890
|
+
if (traceparent)
|
|
891
|
+
metaAdditions.traceparent = traceparent;
|
|
892
|
+
if (ctx.cwd)
|
|
893
|
+
metaAdditions.callerCwd = ctx.cwd;
|
|
894
|
+
}
|
|
895
|
+
if (Object.keys(metaAdditions).length > 0) {
|
|
896
|
+
const existingMeta = params?._meta;
|
|
897
|
+
forwardedParams = {
|
|
898
|
+
...(params || {}),
|
|
899
|
+
_meta: existingMeta && typeof existingMeta === 'object'
|
|
900
|
+
? { ...metaAdditions, ...existingMeta }
|
|
901
|
+
: metaAdditions,
|
|
902
|
+
};
|
|
802
903
|
}
|
|
803
904
|
// Dynamic import to avoid circular dependency
|
|
804
905
|
const { sendCommand } = await import('./daemon/client.js');
|
|
@@ -866,8 +967,18 @@ export class PhotonLoader {
|
|
|
866
967
|
// Inject convenience helpers (render, toast, log, status, progress, thinking)
|
|
867
968
|
injectEmitHelpers(instance);
|
|
868
969
|
}
|
|
869
|
-
|
|
870
|
-
|
|
970
|
+
// Always-inject lazy memory provider. The getter is a pure closure
|
|
971
|
+
// that constructs MemoryProvider on first access — zero cost until
|
|
972
|
+
// used. Gating this on a regex (`detectCapabilities`) missed TS
|
|
973
|
+
// cast shapes with function-type parens like
|
|
974
|
+
// `(this as unknown as { memory: { set: (k: string) => void } })` —
|
|
975
|
+
// the classic TypeScript workaround when memory isn't on the
|
|
976
|
+
// declared class — and silently left `this.memory` undefined. That
|
|
977
|
+
// turned every `.memory.set(...)` call into a data-loss bug that
|
|
978
|
+
// only surfaced on daemon cold start.
|
|
979
|
+
// User-defined memory wins; `in` walks the prototype chain
|
|
980
|
+
// so Photon-base getters aren't silently shadowed.
|
|
981
|
+
if (!('memory' in instance)) {
|
|
871
982
|
const memoryBaseDir = this.baseDir;
|
|
872
983
|
Object.defineProperty(instance, 'memory', {
|
|
873
984
|
get() {
|
|
@@ -879,6 +990,20 @@ export class PhotonLoader {
|
|
|
879
990
|
configurable: true,
|
|
880
991
|
});
|
|
881
992
|
}
|
|
993
|
+
// Always-inject `this.callerCwd`. Reads the originating CLI cwd from
|
|
994
|
+
// the request context, falling back to `process.cwd()` when no context
|
|
995
|
+
// is active (direct in-process load with no caller info). Inside a
|
|
996
|
+
// daemon worker `process.cwd()` is the daemon's cwd, so photons that
|
|
997
|
+
// resolve defaults relative to the user's invocation directory must
|
|
998
|
+
// prefer `this.callerCwd`.
|
|
999
|
+
if (!('callerCwd' in instance)) {
|
|
1000
|
+
Object.defineProperty(instance, 'callerCwd', {
|
|
1001
|
+
get() {
|
|
1002
|
+
return getRequestContext()?.cwd ?? process.cwd();
|
|
1003
|
+
},
|
|
1004
|
+
configurable: true,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
882
1007
|
// Inject call() for cross-photon communication. Always-inject (no
|
|
883
1008
|
// capability-detection gate) because the underlying _callHandler
|
|
884
1009
|
// is ALWAYS wired above. Gating on a regex that matches literal
|
|
@@ -915,6 +1040,13 @@ export class PhotonLoader {
|
|
|
915
1040
|
return client;
|
|
916
1041
|
};
|
|
917
1042
|
}
|
|
1043
|
+
// Always-inject sample/confirm/elicit helpers. These read the
|
|
1044
|
+
// relevant provider from the ALS execution context the loader
|
|
1045
|
+
// populates when it invokes a tool. Injections are pure closures —
|
|
1046
|
+
// zero cost until the method uses them. Matches the always-inject
|
|
1047
|
+
// philosophy: detection-gate misses mean silent breakage, so don't
|
|
1048
|
+
// gate cheap capabilities. User-defined methods win.
|
|
1049
|
+
injectSamplingAndElicitation(instance);
|
|
918
1050
|
// Always-inject caller getter. The prototype check preserves any
|
|
919
1051
|
// user-defined getter on the class (e.g. Photon base class has its
|
|
920
1052
|
// own) so we don't clobber.
|
|
@@ -955,8 +1087,10 @@ export class PhotonLoader {
|
|
|
955
1087
|
},
|
|
956
1088
|
};
|
|
957
1089
|
}
|
|
958
|
-
|
|
959
|
-
|
|
1090
|
+
// Always-inject instance metadata. One file-stat call per load is
|
|
1091
|
+
// cheap and avoids the same detection-gate trap as memory. User-
|
|
1092
|
+
// defined instanceMeta on the class wins.
|
|
1093
|
+
if (!instance.instanceMeta) {
|
|
960
1094
|
const instanceName = options?.instanceName || 'default';
|
|
961
1095
|
const stateFile = getInstanceStatePath(name, instanceName, this.baseDir);
|
|
962
1096
|
try {
|
|
@@ -1309,8 +1443,34 @@ export class PhotonLoader {
|
|
|
1309
1443
|
if (typeof instance._callHandler === 'undefined' || instance._callHandler === undefined) {
|
|
1310
1444
|
const callBaseDir = this.baseDir;
|
|
1311
1445
|
instance._callHandler = async (photonName, method, params, targetInstance) => {
|
|
1446
|
+
// Mirror the primary path: propagate trace context and caller cwd via
|
|
1447
|
+
// in-band `_meta` so the callee resolves relative paths against the
|
|
1448
|
+
// originating CLI directory and traces chain across worker boundaries.
|
|
1449
|
+
const ctxMod = await import('./telemetry/context.js');
|
|
1450
|
+
const ctx = ctxMod.getRequestContext();
|
|
1451
|
+
let forwardedParams = params;
|
|
1452
|
+
const metaAdditions = {};
|
|
1453
|
+
if (ctx) {
|
|
1454
|
+
const traceparent = ctx.parentTraceparent ||
|
|
1455
|
+
(ctx.traceId
|
|
1456
|
+
? `00-${ctx.traceId}-${crypto.randomBytes(8).toString('hex')}-01`
|
|
1457
|
+
: undefined);
|
|
1458
|
+
if (traceparent)
|
|
1459
|
+
metaAdditions.traceparent = traceparent;
|
|
1460
|
+
if (ctx.cwd)
|
|
1461
|
+
metaAdditions.callerCwd = ctx.cwd;
|
|
1462
|
+
}
|
|
1463
|
+
if (Object.keys(metaAdditions).length > 0) {
|
|
1464
|
+
const existingMeta = params?._meta;
|
|
1465
|
+
forwardedParams = {
|
|
1466
|
+
...(params || {}),
|
|
1467
|
+
_meta: existingMeta && typeof existingMeta === 'object'
|
|
1468
|
+
? { ...metaAdditions, ...existingMeta }
|
|
1469
|
+
: metaAdditions,
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1312
1472
|
const { sendCommand } = await import('./daemon/client.js');
|
|
1313
|
-
return sendCommand(photonName, method,
|
|
1473
|
+
return sendCommand(photonName, method, forwardedParams, {
|
|
1314
1474
|
workingDir: callBaseDir,
|
|
1315
1475
|
targetInstance,
|
|
1316
1476
|
});
|
|
@@ -1359,7 +1519,8 @@ export class PhotonLoader {
|
|
|
1359
1519
|
// Inject convenience helpers (render, toast, log, status, progress, thinking)
|
|
1360
1520
|
injectEmitHelpers(instance);
|
|
1361
1521
|
}
|
|
1362
|
-
|
|
1522
|
+
// Always-inject memory (see primary load path for rationale).
|
|
1523
|
+
if (!('memory' in instance)) {
|
|
1363
1524
|
const memoryBaseDir = this.baseDir;
|
|
1364
1525
|
Object.defineProperty(instance, 'memory', {
|
|
1365
1526
|
get() {
|
|
@@ -1371,6 +1532,15 @@ export class PhotonLoader {
|
|
|
1371
1532
|
configurable: true,
|
|
1372
1533
|
});
|
|
1373
1534
|
}
|
|
1535
|
+
// Always-inject callerCwd (see primary load path for rationale).
|
|
1536
|
+
if (!('callerCwd' in instance)) {
|
|
1537
|
+
Object.defineProperty(instance, 'callerCwd', {
|
|
1538
|
+
get() {
|
|
1539
|
+
return getRequestContext()?.cwd ?? process.cwd();
|
|
1540
|
+
},
|
|
1541
|
+
configurable: true,
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1374
1544
|
// Always-inject call() (see the primary load path for rationale).
|
|
1375
1545
|
if (!instance.call) {
|
|
1376
1546
|
instance.call = async (target, params = {}, opts) => {
|
|
@@ -1384,6 +1554,8 @@ export class PhotonLoader {
|
|
|
1384
1554
|
return instance._callHandler(target.slice(0, dotIndex), target.slice(dotIndex + 1), params, opts?.instance);
|
|
1385
1555
|
};
|
|
1386
1556
|
}
|
|
1557
|
+
// Always-inject sample/confirm/elicit (see primary load path).
|
|
1558
|
+
injectSamplingAndElicitation(instance);
|
|
1387
1559
|
}
|
|
1388
1560
|
// Channel event capability: inject on()/off()/_dispatch()/_matchesFilter()
|
|
1389
1561
|
// when source uses this._dispatch( — the universal channel dispatch pattern.
|
|
@@ -2890,29 +3062,63 @@ Run: photon mcp ${mcpName} --config
|
|
|
2890
3062
|
* @returns Tool result, or wrapped result with runId for stateful workflows
|
|
2891
3063
|
*/
|
|
2892
3064
|
async executeTool(mcp, toolName, parameters, options) {
|
|
2893
|
-
// Resolve parentTraceparent from options or in-band `_meta
|
|
2894
|
-
// the ALS context reflects the true parent for any nested `this.call()
|
|
3065
|
+
// Resolve parentTraceparent and callerCwd from options or in-band `_meta`
|
|
3066
|
+
// so the ALS context reflects the true parent for any nested `this.call()`
|
|
3067
|
+
// and the originating CLI invocation directory propagates through worker
|
|
3068
|
+
// boundaries.
|
|
2895
3069
|
let resolvedParentTraceparent = options?.parentTraceparent;
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
typeof parameters === 'object' &&
|
|
2899
|
-
'_meta' in parameters) {
|
|
3070
|
+
let resolvedCallerCwd;
|
|
3071
|
+
if (parameters && typeof parameters === 'object' && '_meta' in parameters) {
|
|
2900
3072
|
const metaPeek = parameters._meta;
|
|
2901
|
-
if (metaPeek
|
|
2902
|
-
resolvedParentTraceparent
|
|
3073
|
+
if (metaPeek) {
|
|
3074
|
+
if (!resolvedParentTraceparent && typeof metaPeek.traceparent === 'string') {
|
|
3075
|
+
resolvedParentTraceparent = metaPeek.traceparent;
|
|
3076
|
+
}
|
|
3077
|
+
if (typeof metaPeek.callerCwd === 'string') {
|
|
3078
|
+
resolvedCallerCwd = metaPeek.callerCwd;
|
|
3079
|
+
}
|
|
2903
3080
|
}
|
|
2904
3081
|
}
|
|
2905
|
-
|
|
3082
|
+
const run = () => runWithRequestContext({
|
|
2906
3083
|
photon: mcp.name,
|
|
2907
3084
|
tool: toolName,
|
|
2908
3085
|
traceId: options?.traceId,
|
|
2909
3086
|
parentTraceparent: resolvedParentTraceparent,
|
|
2910
3087
|
caller: options?.caller,
|
|
3088
|
+
cwd: resolvedCallerCwd,
|
|
2911
3089
|
startedAt: Date.now(),
|
|
2912
3090
|
}, () => this._executeToolInner(mcp, toolName, parameters, {
|
|
2913
3091
|
...options,
|
|
2914
3092
|
parentTraceparent: resolvedParentTraceparent,
|
|
2915
3093
|
}));
|
|
3094
|
+
const gateKey = mcp.instance;
|
|
3095
|
+
if (!gateKey)
|
|
3096
|
+
return run();
|
|
3097
|
+
return this._withInstanceGate(gateKey, run);
|
|
3098
|
+
}
|
|
3099
|
+
/**
|
|
3100
|
+
* Serialize calls targeting the same photon instance. Concurrent callers
|
|
3101
|
+
* queue behind the current in-flight call; each invocation gets an exclusive
|
|
3102
|
+
* slot for the duration of its method body (including all awaits), so two
|
|
3103
|
+
* methods that touch shared state via `this.memory` cannot interleave.
|
|
3104
|
+
*/
|
|
3105
|
+
async _withInstanceGate(instance, fn) {
|
|
3106
|
+
const prev = this._instanceCallTails.get(instance) ?? Promise.resolve();
|
|
3107
|
+
let release;
|
|
3108
|
+
const tail = new Promise((r) => {
|
|
3109
|
+
release = r;
|
|
3110
|
+
});
|
|
3111
|
+
this._instanceCallTails.set(instance, tail);
|
|
3112
|
+
try {
|
|
3113
|
+
await prev;
|
|
3114
|
+
return await fn();
|
|
3115
|
+
}
|
|
3116
|
+
finally {
|
|
3117
|
+
release();
|
|
3118
|
+
if (this._instanceCallTails.get(instance) === tail) {
|
|
3119
|
+
this._instanceCallTails.delete(instance);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
2916
3122
|
}
|
|
2917
3123
|
async _executeToolInner(mcp, toolName, parameters, options) {
|
|
2918
3124
|
// Start audit trail recording
|
|
@@ -3210,9 +3416,19 @@ Run: photon mcp ${mcpName} --config
|
|
|
3210
3416
|
}
|
|
3211
3417
|
let generatorFn = isStatic
|
|
3212
3418
|
? () => method.call(null, ...args)
|
|
3213
|
-
: () => executionContext.run(
|
|
3419
|
+
: () => executionContext.run(
|
|
3420
|
+
// inputProvider and samplingProvider are attached to the ALS
|
|
3421
|
+
// store so photon-core's `this.confirm()`, `this.elicit()`,
|
|
3422
|
+
// and `this.sample()` can find them without every method
|
|
3423
|
+
// having to accept runtime plumbing as an argument. The
|
|
3424
|
+
// @portel/cli ExecutionContext type doesn't declare these
|
|
3425
|
+
// optional fields, so the object is cast through `unknown`;
|
|
3426
|
+
// photon-core reads them back with matching casts.
|
|
3427
|
+
{
|
|
3214
3428
|
outputHandler: outputHandler,
|
|
3215
3429
|
caller: options?.caller,
|
|
3430
|
+
inputProvider,
|
|
3431
|
+
samplingProvider: options?.samplingProvider,
|
|
3216
3432
|
}, () => {
|
|
3217
3433
|
return method.call(mcp.instance, ...args);
|
|
3218
3434
|
});
|