@larkup/workspace-server 0.2.2 → 0.2.9

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.
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../src/app/api/chat/route.ts"],"names":[],"mappings":"AAkHA,eAAO,MAAM,WAAW,MAAM,CAAC;AAktB/B,wBAAsB,IAAI,CAAC,GAAG,EAAE,OAAO,qBAu7CtC"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../src/app/api/chat/route.ts"],"names":[],"mappings":"AAoHA,eAAO,MAAM,WAAW,MAAM,CAAC;AAutB/B,wBAAsB,IAAI,CAAC,GAAG,EAAE,OAAO,qBA26CtC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tool-context.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tool-context.ts"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBpE;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAgB/D;AAED,kFAAkF;AAClF,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBxE;AAmBD,oEAAoE;AACpE,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAuB7F;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CA+D1E;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAyDrE;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CA4KrF;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAgGjF;AAED,sFAAsF;AACtF,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,aAAa,EAAE,2BAA2B,GACzC,MAAM,GAAG,SAAS,CAwFpB;AAED,8FAA8F;AAC9F,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAK,GAAG,MAAM,GAAG,SAAS,CAqE/F;AAED,8EAA8E;AAC9E,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2BvE;AAED,sGAAsG;AACtG,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA0D/F;AAYD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA+IjG;AAED,gGAAgG;AAChG,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,mCAwC/D;AAkLD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,CAapE;AAED,uFAAuF;AACvF,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,CAiB1E;AACD,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4CAA4C,CAAC"}
1
+ {"version":3,"file":"tool-context.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tool-context.ts"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBpE;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAgB/D;AAED,kFAAkF;AAClF,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBxE;AAmBD,oEAAoE;AACpE,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAuB7F;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CA+D1E;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAyDrE;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CA4KrF;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAgGjF;AAED,sFAAsF;AACtF,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,aAAa,EAAE,2BAA2B,GACzC,MAAM,GAAG,SAAS,CAyHpB;AAED,8FAA8F;AAC9F,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAK,GAAG,MAAM,GAAG,SAAS,CAqE/F;AAED,8EAA8E;AAC9E,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2BvE;AAED,sGAAsG;AACtG,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA0D/F;AAYD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA+IjG;AAED,gGAAgG;AAChG,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,mCAwC/D;AAkLD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,CAapE;AAED,uFAAuF;AACvF,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,CAiB1E;AACD,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4CAA4C,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { VideoInvestigationDirective } from '@larkup/core/video-knowledge/query-planner';
2
+ export interface VideoQuestionSignals {
3
+ hasPosition?: boolean;
4
+ connectsBeyondPosition?: boolean;
5
+ asksForSettledOutcome?: boolean;
6
+ }
7
+ /**
8
+ * Reconcile the planner directive with the independent question classifier.
9
+ * The classifier is deliberately language-neutral and has final authority over
10
+ * outcome requests, because a broad planner can otherwise mistake “who won?”
11
+ * for source synthesis and send the answer through appearance rendering.
12
+ */
13
+ export declare function reconcileVideoInvestigation(planned: VideoInvestigationDirective | undefined, signals: VideoQuestionSignals | undefined): VideoInvestigationDirective | undefined;
14
+ //# sourceMappingURL=video-query-planning.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-query-planning.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/video-query-planning.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4CAA4C,CAAC;AAE9F,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,2BAA2B,GAAG,SAAS,EAChD,OAAO,EAAE,oBAAoB,GAAG,SAAS,GACxC,2BAA2B,GAAG,SAAS,CAiBzC"}
@@ -34,7 +34,7 @@ export interface UrlInspection {
34
34
  isYouTube: boolean;
35
35
  }
36
36
  /** Download a public URL after applying the same DNS and redirect checks as media imports. */
37
- export declare function readPublicUrl(url: string, maxBytes?: number): Promise<{
37
+ export declare function readPublicUrl(url: string, maxBytes?: number, init?: RequestInit): Promise<{
38
38
  bytes: Uint8Array;
39
39
  mimeType?: string;
40
40
  finalUrl: string;
@@ -57,6 +57,21 @@ interface YouTubeInspectionData {
57
57
  duration?: number;
58
58
  }>;
59
59
  }
60
+ interface YouTubePlayerData {
61
+ videoDetails?: {
62
+ videoId?: string;
63
+ title?: string;
64
+ lengthSeconds?: string | number;
65
+ };
66
+ streamingData?: {
67
+ formats?: Array<{
68
+ approxDurationMs?: string | number;
69
+ }>;
70
+ adaptiveFormats?: Array<{
71
+ approxDurationMs?: string | number;
72
+ }>;
73
+ };
74
+ }
60
75
  export declare function inspectYouTubeMetadata(url: string, data: YouTubeInspectionData): UrlInspection;
61
76
  /**
62
77
  * A public watch URL is enough to stage one video even when an interactive
@@ -65,6 +80,8 @@ export declare function inspectYouTubeMetadata(url: string, data: YouTubeInspect
65
80
  * fallback does not claim a title, duration, or byte size it could not verify.
66
81
  */
67
82
  export declare function inspectYouTubeWatchUrl(url: string): UrlInspection | undefined;
83
+ export declare function inspectYouTubeWatchPage(url: string, html: string): UrlInspection | undefined;
84
+ export declare function inspectYouTubePlayerMetadata(url: string, data: YouTubePlayerData): UrlInspection | undefined;
68
85
  export interface MediaProbe {
69
86
  durationSecs: number;
70
87
  containerDurationSecs: number;
@@ -1 +1 @@
1
- {"version":3,"file":"source-utils.d.ts","sourceRoot":"","sources":["../../../src/lib/media/source-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"source-utils.d.ts","sourceRoot":"","sources":["../../../src/lib/media/source-utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larkup/workspace-server",
3
- "version": "0.2.2",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -317,16 +317,16 @@
317
317
  "@ai-sdk/mistral": "4.0.61",
318
318
  "@ai-sdk/openai": "4.0.89",
319
319
  "@ai-sdk/openai-compatible": "3.0.66",
320
- "@larkup/core": "^0.8.1",
321
- "@larkup/tool-doc-editor": "^0.3.1",
320
+ "@larkup/core": "^0.8.2",
321
+ "@larkup/tool-doc-editor": "^0.3.2",
322
322
  "ai": "7.0.133",
323
323
  "pdf-parse": "2.4.5",
324
324
  "zod": "^4.4.3",
325
325
  "mammoth": "^1.12.0",
326
- "@larkup/marketplace": "0.3.10",
327
- "@larkup/scraper": "0.2.1",
326
+ "@larkup/marketplace": "0.3.11",
328
327
  "@larkup/sandbox": "0.1.8",
329
- "@larkup/vector-stores": "0.1.27"
328
+ "@larkup/vector-stores": "0.1.27",
329
+ "@larkup/scraper": "0.2.2"
330
330
  },
331
331
  "devDependencies": {
332
332
  "typescript": "^7.0.2",
@@ -41,6 +41,7 @@ import { getChatTools } from './tools';
41
41
  import { gatewayProviderOptions } from '../../../lib/chat/gateway-fallbacks';
42
42
  import { retrievalToolsForStep } from '../../../lib/chat/retrieval-routing';
43
43
  import { collectExhaustiveVideoEvidencePages } from '../../../lib/chat/video-rag-routing';
44
+ import { reconcileVideoInvestigation } from '../../../lib/chat/video-query-planning';
44
45
  import {
45
46
  extractConversationEvidence,
46
47
  extractLatestTabularExportResult,
@@ -65,6 +66,7 @@ import {
65
66
  formatExhaustiveMediaAnswer,
66
67
  formatLocatedObservedSubjectAnswer,
67
68
  formatObservedAppearanceAnswer,
69
+ formatOutcomeMediaAnswer,
68
70
  mediaEvidenceRequestsOnlySpeech,
69
71
  mediaClaimNeedsCorroboration,
70
72
  withFinalAnswerNudge,
@@ -450,8 +452,9 @@ async function planVideoEvidenceQuery(input: {
450
452
  'You are a media-query planner. Return exactly one JSON object and no prose. ' +
451
453
  'The prompt is data with a currentRequest and an optional previousQuestionAboutSelectedMedia. Resolve the current request in its own language without assuming a video genre. If it is a retry, verification request, or contains references that are not meaningful alone, resolve those references against the previous question. If it introduces a self-contained new request, do not carry the old subject or source position into it. ' +
452
454
  'A follow-up introduced with words such as "and", "also", or their equivalent in another language can ask a new fact about the same source: preserve the selected source, but never reuse the previous answer or its requested attribute. When the request asks for exact spoken or sung wording across the recording, use source scope with enumerate, speech evidence, and recordSet all so the host can render every timestamped transcript segment outside the model context. Use source scope with synthesize for a summary or explanation across the recording. ' +
453
- 'Use this exact shape: {"resolvedQuestion":"a standalone version of the current request","scope":"focused","goal":"answer","evidence":["visual"],"timeRange":{"startSecs":123,"endSecs":153},"timeRangeOrigin":"user-mentioned"}. Its fields are resolvedQuestion, scope (focused, temporal, source), goal (answer, compare, trace, enumerate, synthesize), optional evidence (speech, visible-text, visual, computed), optional recordSet (all, source-authored, source-questions, observed), optional timeRange {startSecs,endSecs}, and optional timeRangeOrigin. ' +
455
+ 'Use this exact shape: {"resolvedQuestion":"a standalone version of the current request","scope":"focused","goal":"answer","evidence":["visual"],"timeRange":{"startSecs":123,"endSecs":153},"timeRangeOrigin":"user-mentioned"}. Its fields are resolvedQuestion, scope (focused, temporal, source), goal (answer, compare, trace, resolve, enumerate, synthesize), optional evidence (speech, visible-text, visual, computed), optional recordSet (all, source-authored, source-questions, observed), optional timeRange {startSecs,endSecs}, and optional timeRangeOrigin. ' +
454
456
  'A directive is invalid if the user refers to any source position, time, range, or approximate moment and you omit timeRange: resolve it to numeric seconds and set timeRangeOrigin to user-mentioned, even when the request also asks about other moments. Never emit timeRange or timeRangeOrigin when the user did not constrain a source position; a topic occurring near an opening, closing, or any other position is not a user constraint. When the request names a coarse source unit without a smaller boundary, cover that whole named unit rather than an arbitrary part of it. When the user asks how a visible subject or event at an explicit source position relates to appearances or recurrence elsewhere, use temporal plus trace, visual evidence, and recordSet observed while retaining that position. Use source plus synthesize with no timeRange whenever the answer must combine two or more source facts from unrestricted positions. Use source plus enumerate and recordSet source-questions for a complete inventory of source-authored questions and their answers. Use temporal for a relationship or development across moments; use focused only for one local moment or one self-contained fact. ' +
457
+ 'Use temporal plus resolve when the requested fact is the settled or final outcome of a process, contest, decision, vote, test, or other sequence; this includes asking who won or what the final result was in any language. Do not confuse an entity mentioned in that request with a request to inventory its visual appearances. ' +
455
458
  'Do not answer the question or add fields.',
456
459
  320,
457
460
  );
@@ -461,7 +464,7 @@ async function planVideoEvidenceQuery(input: {
461
464
  // first planning attempt without introducing source- or language-specific
462
465
  // routing rules in the host.
463
466
  return await requestPlan(
464
- 'Return only JSON with resolvedQuestion, scope, goal, and optional recordSet/timeRange/timeRangeOrigin. Resolve an elliptical retry or reference against previousQuestionAboutSelectedMedia; otherwise keep currentRequest standalone. Select source/enumerate/source-questions for a complete question-and-answer inventory from a recording; select source/synthesize for an unrestricted explanation; select focused/answer for one local fact. Do not include a timeRange unless the resolved request contains a source position, and then set timeRangeOrigin to user-mentioned.',
467
+ 'Return only JSON with resolvedQuestion, scope, goal, and optional recordSet/timeRange/timeRangeOrigin. Resolve an elliptical retry or reference against previousQuestionAboutSelectedMedia; otherwise keep currentRequest standalone. Select source/enumerate/source-questions for a complete question-and-answer inventory from a recording; source/synthesize for an unrestricted explanation; temporal/resolve for a settled or final outcome such as who won; focused/answer for one local fact. Do not include a timeRange unless the resolved request contains a source position, and then set timeRangeOrigin to user-mentioned.',
465
468
  180,
466
469
  );
467
470
  } catch {
@@ -486,6 +489,7 @@ async function userSpecifiedSourcePosition(input: {
486
489
  | {
487
490
  hasPosition: boolean;
488
491
  connectsBeyondPosition: boolean;
492
+ asksForSettledOutcome: boolean;
489
493
  }
490
494
  | undefined
491
495
  > {
@@ -498,7 +502,7 @@ async function userSpecifiedSourcePosition(input: {
498
502
  abortSignal: AbortSignal.timeout(8_000),
499
503
  providerOptions: input.providerOptions,
500
504
  system:
501
- 'Return exactly one JSON object and no prose: {"kind":"input-location"|"answer-timestamp"|"none","locator":string|null,"connectsBeyondPosition":boolean}. Use input-location only when the user explicitly constrains where in the provided source to look; locator must be the exact corresponding substring from the user request. Use answer-timestamp when the user asks for timestamps in the answer but does not specify where to look; locator is null. Use none when neither is present. Set connectsBeyondPosition true only when the request explicitly relates the thing at its named source position to another point or portion of the source, such as recurrence, reappearance, a later/earlier occurrence, change, or comparison; otherwise false. Example: "Explain a subject and include timestamps" => {"kind":"answer-timestamp","locator":null,"connectsBeyondPosition":false}. Example: "What happens at 14:00?" => {"kind":"input-location","locator":"14:00","connectsBeyondPosition":false}. Example: "Who is at 14:00 and where else do they appear?" => {"kind":"input-location","locator":"14:00","connectsBeyondPosition":true}. Example: "Give the full story from beginning to conclusion" => {"kind":"none","locator":null,"connectsBeyondPosition":false} unless the user asks specifically about the beginning or conclusion. Interpret every language without assuming a source genre.',
505
+ 'Return exactly one JSON object and no prose: {"kind":"input-location"|"answer-timestamp"|"none","locator":string|null,"connectsBeyondPosition":boolean,"asksForSettledOutcome":boolean}. Use input-location only when the user explicitly constrains where in the provided source to look; locator must be the exact corresponding substring from the user request. Use answer-timestamp when the user asks for timestamps in the answer but does not specify where to look; locator is null. Use none when neither is present. Set connectsBeyondPosition true only when the request explicitly relates the thing at its named source position to another point or portion of the source, such as recurrence, reappearance, a later/earlier occurrence, change, or comparison; otherwise false. Set asksForSettledOutcome true only when the request asks for the final or settled result of a process, contest, decision, vote, test, or other sequence, including who won in any language; it overrides a broad summary classification. A status at a named intermediate position is not a settled outcome. Example: "Explain a subject and include timestamps" => {"kind":"answer-timestamp","locator":null,"connectsBeyondPosition":false,"asksForSettledOutcome":false}. Example: "Who won the match?" => {"kind":"none","locator":null,"connectsBeyondPosition":false,"asksForSettledOutcome":true}. Example: "What happens at 14:00?" => {"kind":"input-location","locator":"14:00","connectsBeyondPosition":false,"asksForSettledOutcome":false}. Example: "Who is at 14:00 and where else do they appear?" => {"kind":"input-location","locator":"14:00","connectsBeyondPosition":true,"asksForSettledOutcome":false}. Example: "Give the full story from beginning to conclusion" => {"kind":"none","locator":null,"connectsBeyondPosition":false,"asksForSettledOutcome":false} unless the user asks specifically about the beginning or conclusion. Interpret every language without assuming a source genre.',
502
506
  prompt: input.question,
503
507
  });
504
508
  const serialized = text.match(/\{[\s\S]*\}/)?.[0];
@@ -507,10 +511,12 @@ async function userSpecifiedSourcePosition(input: {
507
511
  kind?: unknown;
508
512
  locator?: unknown;
509
513
  connectsBeyondPosition?: unknown;
514
+ asksForSettledOutcome?: unknown;
510
515
  };
511
516
  const connectsBeyondPosition = parsed.connectsBeyondPosition === true;
517
+ const asksForSettledOutcome = parsed.asksForSettledOutcome === true;
512
518
  if (parsed.kind === 'answer-timestamp' || parsed.kind === 'none') {
513
- return { hasPosition: false, connectsBeyondPosition };
519
+ return { hasPosition: false, connectsBeyondPosition, asksForSettledOutcome };
514
520
  }
515
521
  if (parsed.kind !== 'input-location' || typeof parsed.locator !== 'string') return undefined;
516
522
  // The quote need only occur in the user text. Its semantic role was
@@ -518,6 +524,7 @@ async function userSpecifiedSourcePosition(input: {
518
524
  return {
519
525
  hasPosition: input.question.normalize('NFKC').includes(parsed.locator.normalize('NFKC')),
520
526
  connectsBeyondPosition,
527
+ asksForSettledOutcome,
521
528
  };
522
529
  } catch {
523
530
  return undefined;
@@ -535,7 +542,7 @@ function parseVideoInvestigationDirective(value: string): VideoInvestigationDire
535
542
  const scope = ['focused', 'temporal', 'source'].includes(String(parsed.scope))
536
543
  ? (parsed.scope as VideoInvestigationDirective['scope'])
537
544
  : 'focused';
538
- const goal = ['answer', 'compare', 'trace', 'enumerate', 'synthesize'].includes(
545
+ const goal = ['answer', 'compare', 'trace', 'resolve', 'enumerate', 'synthesize'].includes(
539
546
  String(parsed.goal),
540
547
  )
541
548
  ? (parsed.goal as VideoInvestigationDirective['goal'])
@@ -1362,6 +1369,7 @@ ${fieldLines}`;
1362
1369
  let preloadedEvidence: unknown;
1363
1370
  let preloadedVideoEvidence = false;
1364
1371
  let preloadedEvidenceQueryAttempted = false;
1372
+ let preloadedVideoInvestigation: VideoInvestigationDirective | undefined;
1365
1373
  const preflightToolCallId = `knowledge-${crypto.randomUUID()}`;
1366
1374
 
1367
1375
  if (directDataExport && dataExportFormat && builtInTools.createDataExport) {
@@ -1700,26 +1708,8 @@ ${fieldLines}`;
1700
1708
  ...plannerInput,
1701
1709
  question: resolvedMediaQuestion,
1702
1710
  });
1703
- const hasUserSpecifiedPosition = sourcePosition?.hasPosition;
1704
- const connectsBeyondPosition = sourcePosition?.connectsBeyondPosition === true;
1705
- // `trace` is already the planner's language-neutral declaration
1706
- // that the answer connects moments. Preserve its supplied source
1707
- // boundary and give the evidence capability its visibility record
1708
- // set even if the auxiliary binary classifier was inconclusive.
1709
- const tracesBeyondPosition =
1710
- connectsBeyondPosition || plannedInvestigation?.goal === 'trace';
1711
- const investigation =
1712
- hasUserSpecifiedPosition === false && plannedInvestigation?.timeRange
1713
- ? { scope: 'source' as const, goal: 'synthesize' as const }
1714
- : tracesBeyondPosition && plannedInvestigation?.timeRange
1715
- ? {
1716
- ...plannedInvestigation,
1717
- scope: 'temporal' as const,
1718
- goal: 'trace' as const,
1719
- evidence: ['visual'] as const,
1720
- recordSet: 'observed' as const,
1721
- }
1722
- : plannedInvestigation;
1711
+ const investigation = reconcileVideoInvestigation(plannedInvestigation, sourcePosition);
1712
+ preloadedVideoInvestigation = investigation;
1723
1713
  const evidenceInput = {
1724
1714
  mediaAssetId,
1725
1715
  query: resolvedMediaQuestion,
@@ -1776,10 +1766,15 @@ ${fieldLines}`;
1776
1766
  const deterministicAnswer =
1777
1767
  formatExhaustiveMediaAnswer(preloadedEvidence, userText) ??
1778
1768
  (useVisualDeterministicAnswer
1779
- ? (formatLocatedObservedSubjectAnswer(preloadedEvidence) ??
1769
+ ? ((preloadedVideoInvestigation
1770
+ ? formatOutcomeMediaAnswer(preloadedEvidence, preloadedVideoInvestigation)
1771
+ : undefined) ??
1772
+ formatLocatedObservedSubjectAnswer(preloadedEvidence) ??
1780
1773
  collectQuestionMatchedDirectClaims(preloadedEvidence, userText)[0] ??
1781
1774
  formatDirectObservationAnswer(preloadedEvidence, userText) ??
1782
- formatObservedAppearanceAnswer(preloadedEvidence))
1775
+ (preloadedVideoInvestigation?.goal === 'resolve'
1776
+ ? undefined
1777
+ : formatObservedAppearanceAnswer(preloadedEvidence)))
1783
1778
  : undefined);
1784
1779
  if (deterministicAnswer) {
1785
1780
  const answerId = `answer-${crypto.randomUUID()}`;
@@ -1328,7 +1328,7 @@ export async function getChatTools(context: {
1328
1328
  investigation: z
1329
1329
  .object({
1330
1330
  scope: z.enum(['focused', 'temporal', 'source']),
1331
- goal: z.enum(['answer', 'compare', 'trace', 'enumerate', 'synthesize']),
1331
+ goal: z.enum(['answer', 'compare', 'trace', 'resolve', 'enumerate', 'synthesize']),
1332
1332
  evidence: z
1333
1333
  .array(z.enum(['speech', 'visible-text', 'visual', 'computed']))
1334
1334
  .max(4)
@@ -2306,7 +2306,7 @@ export async function getChatTools(context: {
2306
2306
  question: z.string().describe('The full user question, including any temporal conditions.'),
2307
2307
  investigation: z.object({
2308
2308
  scope: z.enum(['focused', 'temporal', 'source']),
2309
- goal: z.enum(['answer', 'compare', 'trace', 'enumerate', 'synthesize']),
2309
+ goal: z.enum(['answer', 'compare', 'trace', 'resolve', 'enumerate', 'synthesize']),
2310
2310
  evidence: z
2311
2311
  .array(z.enum(['speech', 'visible-text', 'visual', 'computed']))
2312
2312
  .max(4)
@@ -544,7 +544,7 @@ export function formatOutcomeMediaAnswer(
544
544
  value: unknown,
545
545
  investigation: VideoInvestigationDirective,
546
546
  ): string | undefined {
547
- if (investigation.goal !== 'trace') return undefined;
547
+ if (investigation.goal !== 'trace' && investigation.goal !== 'resolve') return undefined;
548
548
  type Reading = { at: number; text: string };
549
549
  const readings: Reading[] = [];
550
550
  const visit = (candidate: unknown) => {
@@ -575,6 +575,16 @@ export function formatOutcomeMediaAnswer(
575
575
  };
576
576
  visit(value);
577
577
 
578
+ const settledWinner = readings
579
+ .sort((left, right) => left.at - right.at)
580
+ .flatMap((reading) => {
581
+ const match = reading.text.match(
582
+ /^Reconciled event:\s*(?:.*?\b(?:conclusion|final)\b.*?)\bwith\s+(.+?)\s+as\s+(?:the\s+)?winner\s*$/iu,
583
+ );
584
+ return match?.[1]?.trim() ? [{ ...reading, winner: match[1].trim() }] : [];
585
+ })
586
+ .at(-1);
587
+
578
588
  const cleanLabel = (label: string) =>
579
589
  label
580
590
  .replace(/^.*?(?:(?:final\s+)?(?:result|score)|النتيجة(?:\s+النهائية)?)\s*:\s*/iu, '')
@@ -607,6 +617,22 @@ export function formatOutcomeMediaAnswer(
607
617
  : [];
608
618
  });
609
619
  });
620
+ const sideScorePattern =
621
+ /^\s*(.+?\p{L})\s+(\d+(?:[.,]\d+)?)\s*[||]\s*(.+?\p{L})\s+(\d+(?:[.,]\d+)?)\s*$/u;
622
+ const sideScoreCandidates = readings
623
+ .sort((left, right) => left.at - right.at)
624
+ .flatMap((reading) => {
625
+ const match = cleanLabel(reading.text).match(sideScorePattern);
626
+ if (!match) return [];
627
+ const left = { label: cleanLabel(match[1]), value: Number(match[2].replace(',', '.')) };
628
+ const right = { label: cleanLabel(match[3]), value: Number(match[4].replace(',', '.')) };
629
+ return left.label &&
630
+ right.label &&
631
+ Number.isFinite(left.value) &&
632
+ Number.isFinite(right.value)
633
+ ? [{ ...reading, pairs: [left, right] }]
634
+ : [];
635
+ });
610
636
  const pairPattern =
611
637
  /([^=,;،؛\n]{2,100}?)(?:\s*=\s*|\s*:\s*|\s+)(-?\d+(?:[.,]\d+)?)(?=$|[,;،؛])/gu;
612
638
  const candidates = readings
@@ -622,7 +648,14 @@ export function formatOutcomeMediaAnswer(
622
648
  };
623
649
  })
624
650
  .filter((reading) => reading.pairs.length === 2);
625
- const settled = scorelineCandidates.at(-1) ?? candidates.at(-1);
651
+ const settled = scorelineCandidates.at(-1) ?? sideScoreCandidates.at(-1) ?? candidates.at(-1);
652
+ if (settledWinner) {
653
+ if (!settled) return `${settledWinner.winner} won.`;
654
+ const values = settled.pairs.map((pair) => pair.value).sort((left, right) => right - left);
655
+ if (values[0] !== values[1]) {
656
+ return `${settledWinner.winner} won, ${values[0]}–${values[1]}.`;
657
+ }
658
+ }
626
659
  if (!settled) return undefined;
627
660
  const [left, right] = settled.pairs;
628
661
  if (left.value === right.value) {
@@ -0,0 +1,35 @@
1
+ import type { VideoInvestigationDirective } from '@larkup/core/video-knowledge/query-planner';
2
+
3
+ export interface VideoQuestionSignals {
4
+ hasPosition?: boolean;
5
+ connectsBeyondPosition?: boolean;
6
+ asksForSettledOutcome?: boolean;
7
+ }
8
+
9
+ /**
10
+ * Reconcile the planner directive with the independent question classifier.
11
+ * The classifier is deliberately language-neutral and has final authority over
12
+ * outcome requests, because a broad planner can otherwise mistake “who won?”
13
+ * for source synthesis and send the answer through appearance rendering.
14
+ */
15
+ export function reconcileVideoInvestigation(
16
+ planned: VideoInvestigationDirective | undefined,
17
+ signals: VideoQuestionSignals | undefined,
18
+ ): VideoInvestigationDirective | undefined {
19
+ if (signals?.asksForSettledOutcome) {
20
+ return { scope: 'temporal', goal: 'resolve' };
21
+ }
22
+ if (signals?.hasPosition === false && planned?.timeRange) {
23
+ return { scope: 'source', goal: 'synthesize' };
24
+ }
25
+ if ((signals?.connectsBeyondPosition || planned?.goal === 'trace') && planned?.timeRange) {
26
+ return {
27
+ ...planned,
28
+ scope: 'temporal',
29
+ goal: 'trace',
30
+ evidence: ['visual'],
31
+ recordSet: 'observed',
32
+ };
33
+ }
34
+ return planned;
35
+ }
@@ -43,8 +43,9 @@ export interface UrlInspection {
43
43
  export async function readPublicUrl(
44
44
  url: string,
45
45
  maxBytes = 25 * 1024 * 1024,
46
+ init: RequestInit = {},
46
47
  ): Promise<{ bytes: Uint8Array; mimeType?: string; finalUrl: string }> {
47
- const response = await fetchPublic(url);
48
+ const response = await fetchPublic(url, init);
48
49
  if (!response.ok || !response.body) {
49
50
  await response.body?.cancel();
50
51
  throw new Error(`Media download failed (${response.status}).`);
@@ -98,6 +99,18 @@ interface YouTubeInspectionData {
98
99
  }>;
99
100
  }
100
101
 
102
+ interface YouTubePlayerData {
103
+ videoDetails?: {
104
+ videoId?: string;
105
+ title?: string;
106
+ lengthSeconds?: string | number;
107
+ };
108
+ streamingData?: {
109
+ formats?: Array<{ approxDurationMs?: string | number }>;
110
+ adaptiveFormats?: Array<{ approxDurationMs?: string | number }>;
111
+ };
112
+ }
113
+
101
114
  function positiveDuration(value: unknown): number | undefined {
102
115
  const duration = Number(value);
103
116
  return Number.isFinite(duration) && duration > 0 ? duration : undefined;
@@ -170,6 +183,107 @@ export function inspectYouTubeWatchUrl(url: string): UrlInspection | undefined {
170
183
  };
171
184
  }
172
185
 
186
+ export function inspectYouTubeWatchPage(url: string, html: string): UrlInspection | undefined {
187
+ const details = jsonObjectAfterMarker(html, '"videoDetails":');
188
+ if (details) {
189
+ try {
190
+ const inspection = inspectYouTubePlayerMetadata(url, {
191
+ videoDetails: JSON.parse(details) as YouTubePlayerData['videoDetails'],
192
+ });
193
+ if (inspection) return inspection;
194
+ } catch {
195
+ // Continue to the page's public schema.org duration metadata.
196
+ }
197
+ }
198
+ const requestedId = youtubeVideoId(url);
199
+ const durationSecs = youtubePageDurationSecs(html);
200
+ return requestedId && durationSecs
201
+ ? youtubeInspection(url, requestedId, undefined, durationSecs)
202
+ : undefined;
203
+ }
204
+
205
+ export function inspectYouTubePlayerMetadata(
206
+ url: string,
207
+ data: YouTubePlayerData,
208
+ ): UrlInspection | undefined {
209
+ const requestedId = youtubeVideoId(url);
210
+ const details = data.videoDetails;
211
+ const declaredDurationSecs = positiveDuration(details?.lengthSeconds);
212
+ if (requestedId && declaredDurationSecs && details?.videoId === requestedId) {
213
+ return youtubeInspection(url, requestedId, details.title, declaredDurationSecs);
214
+ }
215
+ // Some serverless requests receive a reduced but still valid player payload:
216
+ // `videoDetails` is omitted while the signed selected-stream metadata remains.
217
+ // It is tied to the requested ID in this player request, so use a rounded
218
+ // positive stream duration only when the response does not claim another ID.
219
+ const streamDurationSecs = playerStreamDurationSecs(data);
220
+ if (!requestedId || data.videoDetails?.videoId || !streamDurationSecs) return undefined;
221
+ return youtubeInspection(url, requestedId, data.videoDetails?.title, streamDurationSecs);
222
+ }
223
+
224
+ function youtubeInspection(
225
+ url: string,
226
+ requestedId: string,
227
+ title: string | undefined,
228
+ durationSecs: number,
229
+ ): UrlInspection {
230
+ return {
231
+ originalUrl: url,
232
+ ...(title ? { title } : {}),
233
+ durationSecs,
234
+ singleItemDurationSecs: durationSecs,
235
+ singleItemUrl: `https://www.youtube.com/watch?v=${encodeURIComponent(requestedId)}`,
236
+ entryCount: 1,
237
+ mediaType: 'video',
238
+ isYouTube: true,
239
+ };
240
+ }
241
+
242
+ function playerStreamDurationSecs(data: YouTubePlayerData): number | undefined {
243
+ const durations = [
244
+ ...(data.streamingData?.formats ?? []),
245
+ ...(data.streamingData?.adaptiveFormats ?? []),
246
+ ]
247
+ .map((format) => positiveDuration(Number(format.approxDurationMs) / 1_000))
248
+ .filter((duration): duration is number => duration !== undefined);
249
+ return durations.length ? Math.round(Math.max(...durations)) : undefined;
250
+ }
251
+
252
+ function youtubePageDurationSecs(html: string): number | undefined {
253
+ const durationTag = html.match(
254
+ /<meta\b(?=[^>]*\bitemprop=["']duration["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/i,
255
+ );
256
+ const durationText = durationTag?.[1];
257
+ if (!durationText) return undefined;
258
+ const match = durationText.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i);
259
+ if (!match) return undefined;
260
+ return positiveDuration(
261
+ Number(match[1] ?? 0) * 3_600 + Number(match[2] ?? 0) * 60 + Number(match[3] ?? 0),
262
+ );
263
+ }
264
+
265
+ function jsonObjectAfterMarker(source: string, marker: string): string | undefined {
266
+ const markerIndex = source.indexOf(marker);
267
+ const start = markerIndex < 0 ? -1 : source.indexOf('{', markerIndex + marker.length);
268
+ if (start < 0) return undefined;
269
+ let depth = 0;
270
+ let inString = false;
271
+ let escaped = false;
272
+ for (let index = start; index < source.length; index++) {
273
+ const character = source[index];
274
+ if (inString) {
275
+ if (escaped) escaped = false;
276
+ else if (character === '\\') escaped = true;
277
+ else if (character === '"') inString = false;
278
+ continue;
279
+ }
280
+ if (character === '"') inString = true;
281
+ else if (character === '{') depth += 1;
282
+ else if (character === '}' && --depth === 0) return source.slice(start, index + 1);
283
+ }
284
+ return undefined;
285
+ }
286
+
173
287
  export interface MediaProbe {
174
288
  durationSecs: number;
175
289
  containerDurationSecs: number;
@@ -313,6 +427,48 @@ export function hasInflatedPlaybackTimeline(probe: MediaProbe) {
313
427
  return longestReported - videoDuration > Math.max(5, videoDuration * 0.03);
314
428
  }
315
429
 
430
+ async function inspectYouTubePlayer(url: string): Promise<UrlInspection | undefined> {
431
+ const videoId = youtubeVideoId(url);
432
+ if (!videoId) return undefined;
433
+ const response = await fetchPublic(
434
+ 'https://www.youtube.com/youtubei/v1/player?prettyPrint=false',
435
+ {
436
+ method: 'POST',
437
+ headers: {
438
+ 'content-type': 'application/json',
439
+ 'user-agent': 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip',
440
+ },
441
+ body: JSON.stringify({
442
+ context: {
443
+ client: {
444
+ clientName: 'ANDROID',
445
+ clientVersion: '20.10.38',
446
+ androidSdkVersion: 30,
447
+ hl: 'en',
448
+ gl: 'US',
449
+ },
450
+ },
451
+ videoId,
452
+ contentCheckOk: true,
453
+ racyCheckOk: true,
454
+ }),
455
+ signal: AbortSignal.timeout(MEDIA_URL_INSPECTION_TIMEOUT_MS),
456
+ },
457
+ );
458
+ if (!response.ok) {
459
+ await response.body?.cancel();
460
+ return undefined;
461
+ }
462
+ const declared = Number(response.headers.get('content-length'));
463
+ if (Number.isFinite(declared) && declared > 3 * 1024 * 1024) {
464
+ await response.body?.cancel();
465
+ return undefined;
466
+ }
467
+ const body = await response.text();
468
+ if (body.length > 3 * 1024 * 1024) return undefined;
469
+ return inspectYouTubePlayerMetadata(url, JSON.parse(body) as YouTubePlayerData);
470
+ }
471
+
316
472
  export async function inspectMediaUrl(url: string): Promise<UrlInspection> {
317
473
  const parsed = validHttpUrl(url);
318
474
  await assertPublicHost(parsed.hostname);
@@ -321,6 +477,21 @@ export async function inspectMediaUrl(url: string): Promise<UrlInspection> {
321
477
  try {
322
478
  output = await runYtDlp(['--dump-single-json', '--simulate', '--flat-playlist', url]);
323
479
  } catch (error) {
480
+ const playerInspection = await inspectYouTubePlayer(url).catch(() => undefined);
481
+ if (playerInspection) return playerInspection;
482
+ try {
483
+ const page = await readPublicUrl(url, 3 * 1024 * 1024, {
484
+ headers: {
485
+ // YouTube otherwise serves a reduced challenge page to serverless
486
+ // fetch clients, which excludes its public schema.org duration.
487
+ 'user-agent': 'Mozilla/5.0 (compatible; Larkup/1.0; +https://larkup.de)',
488
+ },
489
+ });
490
+ const inspection = inspectYouTubeWatchPage(url, new TextDecoder().decode(page.bytes));
491
+ if (inspection) return inspection;
492
+ } catch {
493
+ // A public watch URL can still be staged for durable inspection below.
494
+ }
324
495
  const fallback = inspectYouTubeWatchUrl(url);
325
496
  if (fallback) return fallback;
326
497
  throw error;